diff --git a/src/data/history.ts b/src/data/history.ts
index 8b8e708f5c..a631c3423a 100644
--- a/src/data/history.ts
+++ b/src/data/history.ts
@@ -84,6 +84,8 @@ export interface StatisticsMetaData {
statistic_id: string;
source: string;
name?: string | null;
+ has_sum: boolean;
+ has_mean: boolean;
}
export type StatisticsValidationResult =
diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
index 3183133beb..b9b2359dcf 100644
--- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts
+++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
@@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
-import "../../../components/ha-icon-overflow-menu";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
@@ -24,9 +23,9 @@ import {
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
+import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta";
-import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
const FIX_ISSUES_ORDER = {
no_state: 0,
@@ -116,27 +115,21 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
},
actions: {
title: "",
- type: "overflow-menu",
- template: (
- _info,
- statistic: StatisticsMetaData
- ) => html`
- showStatisticsAdjustSumDialog(this, {
- statistic: statistic,
- }),
- },
- ]}
- style="color: var(--secondary-text-color)"
- >`,
+ label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"),
+ type: "icon-button",
+ template: (_info, statistic: StatisticsMetaData) =>
+ statistic.has_sum
+ ? html`
+
+ `
+ : "",
},
})
);
@@ -154,6 +147,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
`;
}
+ private _showStatisticsAdjustSumDialog(ev) {
+ ev.stopPropagation();
+ showStatisticsAdjustSumDialog(this, {
+ statistic: ev.currentTarget.statistic,
+ });
+ }
+
private _rowClicked(ev) {
const id = ev.detail.id;
if (id in this.hass.states) {
@@ -212,6 +212,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
source: "",
state: this.hass.states[statisticId],
issues: issues[statisticId],
+ has_mean: false,
+ has_sum: false,
});
}
});
diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
index 9819177799..bae0576f9e 100644
--- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
+++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
@@ -1,24 +1,37 @@
import "@material/mwc-button/mwc-button";
-import { LitElement, TemplateResult, html, CSSResultGroup } from "lit";
+import "@material/mwc-list/mwc-list-item";
+import { mdiChevronRight } from "@mdi/js";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ PropertyValues,
+ TemplateResult,
+} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
-import "../../../components/ha-dialog";
+import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
+import "../../../components/ha-circular-progress";
+import "../../../components/ha-dialog";
+import "../../../components/ha-form/ha-form";
+import "../../../components/ha-selector/ha-selector-datetime";
+import "../../../components/ha-selector/ha-selector-number";
+import "../../../components/ha-svg-icon";
+import {
+ adjustStatisticsSum,
+ fetchStatistics,
+ StatisticValue,
+} from "../../../data/history";
+import type { DateTimeSelector, NumberSelector } from "../../../data/selector";
+import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
-import "../../../components/ha-formfield";
-import "../../../components/ha-radio";
-import "../../../components/ha-form/ha-form";
-import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
-import type {
- HaFormBaseSchema,
- HaFormSchema,
-} from "../../../components/ha-form/types";
-import { adjustStatisticsSum } from "../../../data/history";
-import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showToast } from "../../../util/toast";
+import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
-let lastMoment: string | undefined;
+/* eslint-disable lit/no-template-arrow */
@customElement("dialog-statistics-adjust-sum")
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
@@ -26,29 +39,54 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
@state() private _params?: DialogStatisticsAdjustSumParams;
- @state() private _data?: {
- moment: string;
- amount: number;
+ @state() private _busy = false;
+
+ @state() private _moment?: string;
+
+ @state() private _stats5min?: StatisticValue[];
+
+ @state() private _statsHour?: StatisticValue[];
+
+ @state() private _chosenStat?: StatisticValue;
+
+ private _origAmount?: number;
+
+ @state() private _amount?: number;
+
+ private _dateTimeSelector: DateTimeSelector = {
+ datetime: {},
};
- @state() private _busy = false;
+ private _amountSelector = memoizeOne(
+ (unit_of_measurement: string): NumberSelector => ({
+ number: {
+ step: 0.01,
+ unit_of_measurement,
+ mode: "box",
+ },
+ })
+ );
public showDialog(params: DialogStatisticsAdjustSumParams): void {
this._params = params;
- this._busy = false;
const now = new Date();
- this._data = {
- moment:
- lastMoment ||
- `${now.getFullYear()}-${
- now.getMonth() + 1
- }-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`,
- amount: 0,
- };
+ this._moment = `${now.getFullYear()}-${
+ now.getMonth() + 1
+ }-${now.getDate()} ${now.getHours()}:${
+ now.getMinutes() - (now.getMinutes() % 5)
+ }:00`;
+ this._fetchStats();
}
public closeDialog(): void {
this._params = undefined;
+ this._moment = undefined;
+ this._stats5min = undefined;
+ this._statsHour = undefined;
+ this._origAmount = undefined;
+ this._amount = undefined;
+ this._chosenStat = undefined;
+ this._busy = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -57,78 +95,201 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
return html``;
}
+ let content: TemplateResult;
+
+ if (!this._chosenStat) {
+ content = this._renderPickStatistic();
+ } else {
+ content = this._renderAdjustStat();
+ }
+
return html`
-
-
-
-
+ ${content}
`;
}
- private _getSchema = memoizeOne((statistic): HaFormSchema[] => [
- {
- type: "constant",
- name: "name",
- value: statistic.name || statistic.statistic_id,
- },
- {
- name: "moment",
- required: true,
- selector: {
- datetime: {},
- },
- },
- {
- name: "amount",
- required: true,
- default: 0,
- selector: {
- number: {
- mode: "box",
- step: 0.1,
- unit_of_measurement: statistic.unit_of_measurement,
- },
- },
- },
- ]);
-
- private _computeLabel(value: HaFormBaseSchema) {
- switch (value.name) {
- case "name":
- return "Statistic";
- case "moment":
- return "Moment to adjust";
- case "amount":
- return "Amount";
- default:
- return value.name;
+ protected shouldUpdate(changedProps: PropertyValues): boolean {
+ if (changedProps.size !== 1 || !changedProps.has("hass")) {
+ return true;
}
+ // We only respond to hass changes if the translations changed
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ return !oldHass || oldHass.localize !== this.hass.localize;
}
- private _valueChanged(ev) {
- this._data = ev.detail.value;
+ private _renderPickStatistic() {
+ let stats: TemplateResult;
+
+ if (!this._stats5min || !this._statsHour) {
+ stats = html``;
+ } else if (this._statsHour.length < 2 && this._stats5min.length < 2) {
+ stats = html`
No statistics found for this period.
`;
+ } else {
+ const data =
+ this._stats5min.length >= 2 ? this._stats5min : this._statsHour;
+ const unit = this._params!.statistic.unit_of_measurement;
+ const rows: TemplateResult[] = [];
+ for (let i = 1; i < data.length; i++) {
+ const stat = data[i];
+ const growth = Math.round((stat.sum! - data[i - 1].sum!) * 100) / 100;
+ rows.push(html`
+ {
+ this._chosenStat = stat;
+ this._origAmount = growth;
+ this._amount = growth;
+ }}
+ >
+ ${growth} ${unit}
+
+ ${formatDateTime(new Date(stat.start), this.hass.locale)}
+
+
+
+ `);
+ }
+ stats = html`${rows}`;
+ }
+
+ return html`
+
+ Sometimes the statistics end up being incorrect for a specific point in
+ time. This can mess up your beautiful graphs! Select a time below to
+ find the bad moment and adjust the data.
+
+
+ ${stats}
+
+ `;
+ }
+
+ private _dateTimeSelectorChanged(ev) {
+ this._moment = ev.detail.value;
+ this._fetchStats();
+ }
+
+ private _renderAdjustStat() {
+ return html`
+
+ ${this._params!.statistic.name || this._params!.statistic.statistic_id}
+
+
+
+ Start
+ ${formatDateTime(
+ new Date(this._chosenStat!.start),
+ this.hass.locale
+ )}
+
+
+
+ End
+ ${formatDateTime(
+ new Date(this._chosenStat!.end),
+ this.hass.locale
+ )}
+
+
+ {
+ this._amount = ev.detail.value;
+ }}
+ >
+
+ {
+ this._fixIssue();
+ }}
+ >
+ {
+ this._chosenStat = undefined;
+ }}
+ >
+ `;
+ }
+
+ private async _fetchStats(): Promise {
+ this._stats5min = undefined;
+ this._statsHour = undefined;
+ const statId = this._params!.statistic.statistic_id;
+ const moment = new Date(this._moment!);
+
+ // Search 3 hours before and 3 hours after chosen time
+ const hourStatStart = new Date(moment.getTime());
+ hourStatStart.setTime(hourStatStart.getTime() - 3 * 3600 * 1000);
+ const hourStatEnd = new Date(moment.getTime());
+ hourStatEnd.setTime(hourStatEnd.getTime() + 3 * 3600 * 1000);
+
+ const statsHourData = await fetchStatistics(
+ this.hass,
+ hourStatStart,
+ hourStatEnd,
+ [statId],
+ "hour"
+ );
+ this._statsHour =
+ statId in statsHourData ? statsHourData[statId].slice(0, 6) : [];
+
+ // Can't have 5 min data if no hourly data
+ if (this._statsHour.length === 0) {
+ this._stats5min = [];
+ return;
+ }
+
+ // Search 15 minutes before and 15 minutes after chosen time
+ const minStatStart = new Date(moment.getTime());
+ minStatStart.setTime(minStatStart.getTime() - 15 * 60 * 1000);
+ const minStatEnd = new Date(moment.getTime());
+ minStatEnd.setTime(minStatEnd.getTime() + 15 * 60 * 1000);
+
+ const stats5MinData = await fetchStatistics(
+ this.hass,
+ minStatStart,
+ minStatEnd,
+ [statId],
+ "5minute"
+ );
+
+ this._stats5min =
+ statId in stats5MinData ? stats5MinData[statId].slice(0, 6) : [];
}
private async _fixIssue(): Promise {
@@ -137,8 +298,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
await adjustStatisticsSum(
this.hass,
this._params!.statistic.statistic_id,
- this._data!.moment,
- this._data!.amount
+ this._chosenStat!.start,
+ this._amount! - this._origAmount!
);
} catch (err: any) {
this._busy = false;
@@ -150,12 +311,59 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
showToast(this, {
message: "Statistic sum adjusted",
});
- lastMoment = this._data!.moment;
this.closeDialog();
}
static get styles(): CSSResultGroup {
- return [haStyle, haStyleDialog];
+ return [
+ haStyle,
+ haStyleDialog,
+ css`
+ @media all and (max-width: 450px), all and (max-height: 500px) {
+ /* overrule the ha-style-dialog max-height on small screens */
+ ha-dialog {
+ --mdc-dialog-max-height: 100%;
+ height: 100%;
+ }
+ }
+
+ @media all and (min-width: 850px) {
+ ha-dialog {
+ --mdc-dialog-max-height: 80%;
+ --mdc-dialog-max-height: 80%;
+ }
+ }
+
+ @media all and (min-width: 451px) and (min-height: 501px) {
+ ha-dialog {
+ --mdc-dialog-max-width: 480px;
+ }
+ }
+
+ .text-content,
+ ha-selector-datetime,
+ ha-selector-number {
+ margin-bottom: 20px;
+ }
+ mwc-list-item {
+ margin: 0 -24px;
+ --mdc-list-side-padding: 24px;
+ }
+ .table-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ }
+ .stat-list {
+ min-height: 360px;
+ display: flex;
+ flex-direction: column;
+ }
+ .stat-list ha-circular-progress {
+ margin: 0 auto;
+ }
+ `,
+ ];
}
}