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

Merged
merged 4 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
147 changes: 147 additions & 0 deletions src/data/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { computeStateNameFromEntityAttributes } from "../common/entity/compute_s
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import type { FrontendLocaleData } from "./translation";
import type { Statistics } from "./recorder";

const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const NEED_ATTRIBUTE_DOMAINS = [
Expand Down Expand Up @@ -417,6 +418,54 @@ const isNumericSensorEntity = (

const BLANK_UNIT = " ";

export const convertStatisticsToHistory = (
hass: HomeAssistant,
statistics: Statistics,
statisticIds: string[],
sensorNumericDeviceClasses: string[],
splitDeviceClasses = false
): HistoryResult => {
// Maintain the statistic id ordering
const orderedStatistics: Statistics = {};
statisticIds.forEach((id) => {
if (id in statistics) {
orderedStatistics[id] = statistics[id];
}
});

// Convert statistics to HistoryResult format
const statsHistoryStates: HistoryStates = {};
Object.entries(orderedStatistics).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;
});

const statisticsHistory = computeHistory(
hass,
statsHistoryStates,
[],
hass.localize,
sensorNumericDeviceClasses,
splitDeviceClasses,
true
);

// remap states array to statistics array
(statisticsHistory?.line || []).forEach((item) => {
item.data.forEach((data) => {
data.statistics = data.states;
data.states = [];
});
});

return statisticsHistory;
};

export const computeHistory = (
hass: HomeAssistant,
stateHistory: HistoryStates,
Expand Down Expand Up @@ -564,3 +613,101 @@ export const isNumericEntity = (
domain === "sensor" &&
isNumericSensorEntity(currentState, sensorNumericalDeviceClasses)) ||
numericStateFromHistory != null;

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;
};
141 changes: 10 additions & 131 deletions src/panels/history/ha-panel-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,13 @@ import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import type {
EntityHistoryState,
HistoryResult,
HistoryStates,
LineChartState,
LineChartUnit,
} from "../../data/history";
import type { HistoryResult } from "../../data/history";
import {
computeGroupKey,
computeHistory,
subscribeHistory,
mergeHistoryResults,
convertStatisticsToHistory,
} from "../../data/history";
import type { Statistics } from "../../data/recorder";
import { fetchStatistics } from "../../data/recorder";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
Expand Down Expand Up @@ -210,92 +204,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 +215,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 Expand Up @@ -410,45 +318,16 @@ class HaPanelHistory extends LitElement {
["mean", "state"]
);

// Maintain the statistic id ordering
const orderedStatistics: Statistics = {};
statisticIds.forEach((id) => {
if (id in statistics) {
orderedStatistics[id] = statistics[id];
}
});

// Convert statistics to HistoryResult format
const statsHistoryStates: HistoryStates = {};
Object.entries(orderedStatistics).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;
});

const { numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass);

this._statisticsHistory = computeHistory(
this.hass,
statsHistoryStates,
[],
this.hass.localize,
this._statisticsHistory = convertStatisticsToHistory(
this.hass!,
statistics,
statisticIds,
sensorNumericDeviceClasses,
true,
true
);
// remap states array to statistics array
(this._statisticsHistory?.line || []).forEach((item) => {
item.data.forEach((data) => {
data.statistics = data.states;
data.states = [];
});
});
}

private async _getHistory() {
Expand Down
Loading
Loading