Convert history header/footer to use streaming history (#15136)

* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* fixes

* cleanup

* redraw

* naming is hard

* drop cached history

* backport

* Convert history header/footer to use streaming history

needs #15112

* Update src/data/history.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Update src/data/history.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* review

* review

* review

* review

* review

* review

* review

* review

* adjust

* Revert "adjust"

This reverts commit 6ba31da4a5a619a0da1bfbcfe18723de595e19aa.

* move setInterval

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
J. Nick Koston 2023-01-21 17:58:12 -10:00 committed by GitHub
parent 815d4c165d
commit 2b2dd74672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 110 additions and 81 deletions

View File

@ -1,4 +1,5 @@
import { strokeWidth } from "../../../../data/graph";
import { EntityHistoryState } from "../../../../data/history";
const average = (items: any[]): number =>
items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / items.length;
@ -105,3 +106,25 @@ export const coordinates = (
return calcPoints(history, hours, width, detail, min, max);
};
interface NumericEntityHistoryState {
state: number;
last_changed: number;
}
export const coordinatesMinimalResponseCompressedState = (
history: EntityHistoryState[],
hours: number,
width: number,
detail: number,
limits?: { min?: number; max?: number }
): number[][] | undefined => {
const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({
state: Number(item.s),
// With minimal response and compressed state, we don't have last_changed,
// so we use last_updated since its always the same as last_changed since
// we already filtered out states that are the same.
last_changed: item.lu * 1000,
}));
return coordinates(numericHistory, hours, width, detail, limits);
};

View File

@ -9,18 +9,18 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-circular-progress";
import { fetchRecent } from "../../../data/history";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
import { computeDomain } from "../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { coordinates } from "../common/graph/coordinates";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { coordinatesMinimalResponseCompressedState } from "../common/graph/coordinates";
import "../components/hui-graph-base";
import { LovelaceHeaderFooter, LovelaceHeaderFooterEditor } from "../types";
import { GraphHeaderFooterConfig } from "./types";
const MINUTE = 60000;
const HOUR = MINUTE * 60;
const HOUR = 60 * MINUTE;
const includeDomains = ["counter", "input_number", "number", "sensor"];
@customElement("hui-graph-header-footer")
@ -66,11 +66,11 @@ export class HuiGraphHeaderFooter
@state() private _coordinates?: number[][];
private _date?: Date;
private _error?: string;
private _stateHistory?: HassEntity[];
private _interval?: number;
private _fetching = false;
private _subscribed?: Promise<(() => Promise<void>) | void>;
public getCardSize(): number {
return 3;
@ -104,6 +104,10 @@ export class HuiGraphHeaderFooter
return html``;
}
if (this._error) {
return html`<div class="errors">${this._error}</div>`;
}
if (!this._coordinates) {
return html`
<div class="container">
@ -125,89 +129,91 @@ export class HuiGraphHeaderFooter
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._subscribeHistoryTimeWindow();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeHistoryTimeWindow();
}
private _subscribeHistoryTimeWindow() {
if (!isComponentLoaded(this.hass!, "history") || this._subscribed) {
return;
}
this._subscribed = subscribeHistoryStatesTimeWindow(
this.hass!,
(combinedHistory) => {
if (!this._subscribed) {
// Message came in before we had a chance to unload
return;
}
this._coordinates =
coordinatesMinimalResponseCompressedState(
combinedHistory[this._config!.entity],
this._config!.hours_to_show!,
500,
this._config!.detail!,
this._config!.limits
) || [];
},
this._config!.hours_to_show!,
[this._config!.entity]
).catch((err) => {
this._subscribed = undefined;
this._error = err;
});
this._setRedrawTimer();
}
private _redrawGraph() {
if (this._coordinates) {
this._coordinates = [...this._coordinates];
}
}
private _setRedrawTimer() {
// redraw the graph every minute to update the time axis
clearInterval(this._interval);
this._interval = window.setInterval(
() => this._redrawGraph(),
this._config!.hours_to_show! > 24 ? HOUR : MINUTE
);
}
private _unsubscribeHistoryTimeWindow() {
clearInterval(this._interval);
if (!this._subscribed) {
return;
}
this._subscribed.then((unsubscribe) => {
if (unsubscribe) {
unsubscribe();
}
this._subscribed = undefined;
});
}
protected updated(changedProps: PropertyValues) {
if (
!this._config ||
!this.hass ||
(this._fetching && !changedProps.has("_config"))
) {
if (!this._config || !this.hass || !changedProps.has("_config")) {
return;
}
if (changedProps.has("_config")) {
const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig;
if (!oldConfig || oldConfig.entity !== this._config.entity) {
this._stateHistory = [];
}
this._getCoordinates();
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
this._getCoordinates();
const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig;
if (
!oldConfig ||
!this._subscribed ||
oldConfig.entity !== this._config.entity
) {
this._unsubscribeHistoryTimeWindow();
this._subscribeHistoryTimeWindow();
}
}
private async _getCoordinates(): Promise<void> {
this._fetching = true;
const endTime = new Date();
const startTime =
!this._date || !this._stateHistory?.length
? new Date(
new Date().setHours(
endTime.getHours() - this._config!.hours_to_show!
)
)
: this._date;
if (this._stateHistory!.length) {
const inHoursToShow: HassEntity[] = [];
const outHoursToShow: HassEntity[] = [];
// Split into inside and outside of "hours to show".
this._stateHistory!.forEach((entity) =>
(endTime.getTime() - new Date(entity.last_changed).getTime() <=
this._config!.hours_to_show! * HOUR
? inHoursToShow
: outHoursToShow
).push(entity)
);
if (outHoursToShow.length) {
// If we have values that are now outside of "hours to show", re-add the last entry. This could e.g. be
// the "initial state" from the history backend. Without it, it would look like there is no history data
// at the start at all in the database = graph would start suddenly instead of on the left side of the card.
inHoursToShow.push(outHoursToShow[outHoursToShow.length - 1]);
}
this._stateHistory = inHoursToShow;
}
const stateHistory = await fetchRecent(
this.hass!,
this._config!.entity,
startTime,
endTime,
Boolean(this._stateHistory!.length)
);
if (stateHistory.length && stateHistory[0].length) {
this._stateHistory!.push(...stateHistory[0]);
}
this._coordinates =
coordinates(
this._stateHistory,
this._config!.hours_to_show!,
500,
this._config!.detail!,
this._config!.limits
) || [];
this._date = endTime;
this._fetching = false;
}
static get styles(): CSSResultGroup {
return css`
ha-circular-progress {