Clean up the overlapping history in the CSV download (#19622)

* Clean up the overlapping history in the CSV download

* Speed up merge

* undefined unit

* Fix targetPickerValue handling
This commit is contained in:
Paulus Schoutsen 2024-02-02 10:06:29 -05:00 committed by GitHub
parent e478038206
commit 682f9a0f04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,6 +6,7 @@ import {
} from "home-assistant-js-websocket/dist/types";
import { LitElement, PropertyValues, css, html } from "lit";
import { property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { navigate } from "../../common/navigate";
@ -44,8 +45,8 @@ import {
HistoryStates,
EntityHistoryState,
LineChartUnit,
LineChartEntity,
computeGroupKey,
LineChartState,
} from "../../data/history";
import { fetchStatistics, Statistics } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
@ -71,7 +72,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
state: true,
subscribe: false,
})
private _targetPickerValue?: HassServiceTarget;
private _targetPickerValue: HassServiceTarget = {};
@state() private _isLoading = false;
@ -138,6 +139,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
protected render() {
const entitiesSelected = this._getEntityIds().length > 0;
return html`
<ha-top-app-bar-fixed>
${this._showBack
@ -155,7 +157,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
></ha-menu-button>
`}
<div slot="title">${this.hass.localize("panel.history")}</div>
${this._targetPickerValue
${entitiesSelected
? html`
<ha-icon-button
slot="actionItems"
@ -196,7 +198,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
? html`<div class="progress-wrapper">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: !this._targetPickerValue
: !entitiesSelected
? html`<div class="start-search">
${this.hass.localize("ui.panel.history.start_search")}
</div>`
@ -220,50 +222,83 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
): HistoryResult {
const result: HistoryResult = { ...historyResult, line: [] };
const keys = new Set(
historyResult.line
.map((i) => computeGroupKey(i.unit, i.device_class, true))
.concat(
ltsResult.line.map((i) =>
computeGroupKey(i.unit, i.device_class, true)
)
)
);
keys.forEach((key) => {
const historyItem = historyResult.line.find(
(i) => computeGroupKey(i.unit, i.device_class, true) === key
);
const ltsItem = ltsResult.line.find(
(i) => computeGroupKey(i.unit, i.device_class, true) === key
);
if (historyItem && ltsItem) {
const lookup: Record<
string,
{ historyItem?: LineChartUnit; ltsItem?: LineChartUnit }
> = {};
for (const item of historyResult.line) {
const key = computeGroupKey(item.unit, item.device_class, true);
if (key) {
lookup[key] = {
historyItem: item,
};
}
}
for (const item of ltsResult.line) {
const key = computeGroupKey(item.unit, item.device_class, true);
if (!key) {
continue;
}
if (key in lookup) {
lookup[key].ltsItem = item;
} else {
lookup[key] = { ltsItem: item };
}
}
for (const { historyItem, ltsItem } of Object.values(lookup)) {
if (!historyItem || !ltsItem) {
// Only one result has data for this item, so just push it directly instead of merging.
result.line.push(historyItem || ltsItem!);
continue;
}
const newLineItem: LineChartUnit = { ...historyItem, data: [] };
const entities = new Set(
historyItem.data
.map((d) => d.entity_id)
.concat(ltsItem.data.map((d) => d.entity_id))
);
entities.forEach((entity) => {
const entities = new Set([
...historyItem.data.map((d) => d.entity_id),
...ltsItem.data.map((d) => d.entity_id),
]);
for (const entity of entities) {
const historyDataItem = historyItem.data.find(
(d) => d.entity_id === entity
);
const ltsDataItem = ltsItem.data.find((d) => d.entity_id === entity);
if (historyDataItem && ltsDataItem) {
const newDataItem: LineChartEntity = {
...historyDataItem,
statistics: ltsDataItem.statistics,
};
newLineItem.data.push(newDataItem);
} else {
if (!historyDataItem || !ltsDataItem) {
newLineItem.data.push(historyDataItem || ltsDataItem!);
continue;
}
// Remove statistics that overlap with states
const oldestState =
historyDataItem.states[0]?.last_changed ||
// If no state, fall back to the max last changed of the last statistics (so approve all)
ltsDataItem.statistics![ltsDataItem.statistics!.length - 1]
.last_changed + 1;
const statistics: LineChartState[] = [];
for (const s of ltsDataItem.statistics!) {
if (s.last_changed >= oldestState) {
break;
}
statistics.push(s);
}
newLineItem.data.push(
statistics.length === 0
? // All statistics overlapped with states, so just push the states
historyDataItem
: {
...historyDataItem,
statistics,
}
);
}
});
result.line.push(newLineItem);
} else {
// Only one result has data for this item, so just push it directly instead of merging.
result.line.push(historyItem || ltsItem!);
}
});
return result;
}
@ -335,14 +370,13 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
this._targetPickerValue &&
(changedProps.has("_startDate") ||
changedProps.has("_startDate") ||
changedProps.has("_endDate") ||
changedProps.has("_targetPickerValue") ||
(!this._stateHistory &&
(changedProps.has("_deviceEntityLookup") ||
changedProps.has("_areaEntityLookup") ||
changedProps.has("_areaDeviceLookup"))))
changedProps.has("_areaDeviceLookup")))
) {
this._getHistory();
this._getStats();
@ -350,18 +384,18 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
private _removeAll() {
this._targetPickerValue = undefined;
this._targetPickerValue = {};
this._updatePath();
}
private async _getStats() {
const statisticIds = this._getEntityIds();
if (!statisticIds) {
if (statisticIds.length === 0) {
this._statisticsHistory = undefined;
return;
}
try {
const statistics = await fetchStatistics(
this.hass!,
this._startDate,
@ -409,15 +443,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
data.states = [];
});
});
} catch (err) {
this._statisticsHistory = undefined;
}
}
private async _getHistory() {
const entityIds = this._getEntityIds();
if (!entityIds) {
if (entityIds.length === 0) {
this._stateHistory = undefined;
return;
}
@ -485,14 +516,29 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
}
private _getEntityIds(): string[] | undefined {
private _getEntityIds(): string[] {
return this.__getEntityIds(
this._targetPickerValue,
this._deviceEntityLookup,
this._areaEntityLookup,
this._areaDeviceLookup
);
}
private __getEntityIds = memoizeOne(
(
targetPickerValue: HassServiceTarget,
deviceEntityLookup: DeviceEntityLookup | undefined,
areaEntityLookup: AreaEntityLookup | undefined,
areaDeviceLookup: AreaDeviceLookup | undefined
): string[] => {
if (
!this._targetPickerValue ||
this._deviceEntityLookup === undefined ||
this._areaEntityLookup === undefined ||
this._areaDeviceLookup === undefined
!targetPickerValue ||
deviceEntityLookup === undefined ||
areaEntityLookup === undefined ||
areaDeviceLookup === undefined
) {
return undefined;
return [];
}
const entityIds = new Set<string>();
@ -500,12 +546,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
area_id: searchingAreaId,
device_id: searchingDeviceId,
entity_id: searchingEntityId,
} = this._targetPickerValue;
} = targetPickerValue;
if (searchingAreaId) {
searchingAreaId = ensureArray(searchingAreaId);
for (const singleSearchingAreaId of searchingAreaId) {
const foundEntities = this._areaEntityLookup[singleSearchingAreaId];
const foundEntities = areaEntityLookup[singleSearchingAreaId];
if (foundEntities?.length) {
for (const foundEntity of foundEntities) {
if (foundEntity.entity_category === null) {
@ -514,13 +560,13 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
}
const foundDevices = this._areaDeviceLookup[singleSearchingAreaId];
const foundDevices = areaDeviceLookup[singleSearchingAreaId];
if (!foundDevices?.length) {
continue;
}
for (const foundDevice of foundDevices) {
const foundDeviceEntities = this._deviceEntityLookup[foundDevice.id];
const foundDeviceEntities = deviceEntityLookup[foundDevice.id];
if (!foundDeviceEntities?.length) {
continue;
}
@ -541,7 +587,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
if (searchingDeviceId) {
searchingDeviceId = ensureArray(searchingDeviceId);
for (const singleSearchingDeviceId of searchingDeviceId) {
const foundEntities = this._deviceEntityLookup[singleSearchingDeviceId];
const foundEntities = deviceEntityLookup[singleSearchingDeviceId];
if (!foundEntities?.length) {
continue;
}
@ -563,6 +609,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
return [...entityIds];
}
);
private _dateRangeChanged(ev) {
this._startDate = ev.detail.startDate;
@ -584,7 +631,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
private _updatePath() {
const params: Record<string, string> = {};
if (this._targetPickerValue) {
if (this._targetPickerValue.entity_id) {
params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
","
@ -598,7 +644,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
","
);
}
}
if (this._startDate) {
params.start_date = this._startDate.toISOString();
@ -613,7 +658,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
private _downloadHistory() {
const entities = this._getEntityIds();
if (!entities || !this._mungedStateHistory) {
if (entities.length === 0 || !this._mungedStateHistory) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.history.download_data_error"),
text: this.hass.localize("ui.panel.history.error_no_data"),
@ -628,14 +673,16 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
for (const line of this._mungedStateHistory.line) {
for (const entity of line.data) {
const entityId = entity.entity_id;
for (const data of [entity.states, entity.statistics]) {
if (!data) {
continue;
}
for (const s of data) {
if (entity.statistics) {
for (const s of entity.statistics) {
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
}
}
for (const s of entity.states) {
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
}
}
}
for (const timeline of this._mungedStateHistory.timeline) {