Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use statistics data in History graph card to fill gaps #23612

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/components/chart/state-history-charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ declare global {
export class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;

@property({ attribute: false }) public historyData!: HistoryResult;
@property({ attribute: false }) public historyData?: HistoryResult;

@property({ type: Boolean }) public narrow = false;

Expand Down Expand Up @@ -119,12 +119,12 @@ export class StateHistoryCharts extends LitElement {
${this.hass.localize("ui.components.history_charts.no_history_found")}
</div>`;
}
const combinedItems = this.historyData.timeline.length
const combinedItems = this.historyData!.timeline.length
? (this.virtualize
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
: [this.historyData.timeline]
).concat(this.historyData.line)
: this.historyData.line;
? chunkData(this.historyData!.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
: [this.historyData!.timeline]
).concat(this.historyData!.line)
: this.historyData!.line;

// eslint-disable-next-line lit/no-this-assign-in-render
this._chartCount = combinedItems.length;
Expand Down
98 changes: 98 additions & 0 deletions src/data/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,101 @@ export const computeGroupKey = (
device_class: string | undefined,
splitDeviceClasses: boolean
) => (splitDeviceClasses ? `${unit}_${device_class || ""}` : unit);

export const mergeHistoryResults = (
historyResult: HistoryResult,
ltsResult?: HistoryResult,
splitDeviceClasses = true
): HistoryResult => {
if (!ltsResult) {
return historyResult;
}
const result: HistoryResult = { ...historyResult, line: [] };

const lookup: Record<
string,
{ historyItem?: LineChartUnit; ltsItem?: LineChartUnit }
> = {};

for (const item of historyResult.line) {
const key = computeGroupKey(
item.unit,
item.device_class,
splitDeviceClasses
);
if (key) {
lookup[key] = {
historyItem: item,
};
}
}

for (const item of ltsResult.line) {
const key = computeGroupKey(
item.unit,
item.device_class,
splitDeviceClasses
);
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),
...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) {
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);
}
return result;
};
96 changes: 4 additions & 92 deletions src/panels/history/ha-panel-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ import type {
EntityHistoryState,
HistoryResult,
HistoryStates,
LineChartState,
LineChartUnit,
} from "../../data/history";
import {
computeGroupKey,
computeHistory,
subscribeHistory,
mergeHistoryResults,
} from "../../data/history";
import type { Statistics } from "../../data/recorder";
import { fetchStatistics } from "../../data/recorder";
Expand Down Expand Up @@ -210,92 +208,6 @@ class HaPanelHistory extends LitElement {
`;
}

private _mergeHistoryResults(
ltsResult: HistoryResult,
historyResult: HistoryResult
): HistoryResult {
const result: HistoryResult = { ...historyResult, line: [] };

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),
...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) {
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);
}
return result;
}

public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);

Expand All @@ -307,9 +219,9 @@ class HaPanelHistory extends LitElement {
changedProps.has("_targetPickerValue")
) {
if (this._statisticsHistory && this._stateHistory) {
this._mungedStateHistory = this._mergeHistoryResults(
this._statisticsHistory,
this._stateHistory
this._mungedStateHistory = mergeHistoryResults(
this._stateHistory,
this._statisticsHistory
);
} else {
this._mungedStateHistory =
Expand Down
74 changes: 67 additions & 7 deletions src/panels/lovelace/cards/hui-history-graph-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import "../../../components/chart/state-history-charts";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import type { HistoryResult } from "../../../data/history";
import {
computeHistory,
subscribeHistoryStatesTimeWindow,
type HistoryResult,
type HistoryStates,
type EntityHistoryState,
mergeHistoryResults,
} from "../../../data/history";
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
import type { HomeAssistant } from "../../../types";
Expand All @@ -19,6 +22,7 @@ import { processConfigEntities } from "../common/process-config-entities";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { HistoryGraphCardConfig } from "./types";
import { createSearchParam } from "../../../common/url/search-params";
import { fetchStatistics } from "../../../data/recorder";

export const DEFAULT_HOURS_TO_SHOW = 24;

Expand All @@ -36,7 +40,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {

@property({ attribute: false }) public hass?: HomeAssistant;

@state() private _stateHistory?: HistoryResult;
@state() private _history?: HistoryResult;

@state() private _statisticsHistory?: HistoryResult;

@state() private _config?: HistoryGraphCardConfig;

Expand Down Expand Up @@ -118,27 +124,81 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return;
}

this._stateHistory = computeHistory(
const stateHistory = computeHistory(
this.hass!,
combinedHistory,
this._entityIds,
this.hass!.localize,
sensorNumericDeviceClasses,
this._config?.split_device_classes
);

this._history = mergeHistoryResults(
stateHistory,
this._statisticsHistory,
this._config?.split_device_classes
);
},
this._hoursToShow,
this._entityIds
).catch((err) => {
this._subscribed = undefined;
this._error = err;
});

await this._fetchStatistics(sensorNumericDeviceClasses);

this._setRedrawTimer();
}

private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
const now = new Date();
const start = new Date();
start.setHours(start.getHours() - this._hoursToShow);

const statistics = await fetchStatistics(
this.hass!,
start,
now,
this._entityIds,
"hour",
undefined,
["mean", "state"]
);

// Convert statistics to HistoryResult format
const statsHistoryStates: HistoryStates = {};
Object.entries(statistics).forEach(([key, value]) => {
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
lc: e.start / 1000,
a: {},
lu: e.start / 1000,
}));
statsHistoryStates[key] = entityHistoryStates;
});

this._statisticsHistory = computeHistory(
this.hass!,
statsHistoryStates,
[],
this.hass!.localize,
sensorNumericDeviceClasses,
this._config?.split_device_classes
);

// remap states array to statistics array
(this._statisticsHistory?.line || []).forEach((item) => {
item.data.forEach((data) => {
data.statistics = data.states;
data.states = [];
});
});
}
MindFreeze marked this conversation as resolved.
Show resolved Hide resolved

private _redrawGraph() {
if (this._stateHistory) {
this._stateHistory = { ...this._stateHistory };
if (this._history) {
this._history = { ...this._history };
}
}

Expand Down Expand Up @@ -229,8 +289,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
: html`
<state-history-charts
.hass=${this.hass}
.isLoadingData=${!this._stateHistory}
.historyData=${this._stateHistory}
.isLoadingData=${!this._history}
.historyData=${this._history}
.names=${this._names}
up-to-now
.hoursToShow=${this._hoursToShow}
Expand Down
Loading