From a89caccd326b021ff83b3e07d3a67b81b26d2a3f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 27 Sep 2021 22:03:57 +0200 Subject: [PATCH] Statistics dev tools (#10074) * Statistics dev tools * Show all statistics * Update src/data/history.ts Co-authored-by: Paulus Schoutsen * Update history.ts Co-authored-by: Paulus Schoutsen --- src/components/data-table/ha-data-table.ts | 2 +- src/data/history.ts | 39 ++++ .../developer-tools/developer-tools-router.ts | 4 + .../ha-panel-developer-tools.ts | 5 + .../statistics/developer-tools-statistics.ts | 204 ++++++++++++++++++ .../dialog-statistics-fix-units-changed.ts | 122 +++++++++++ ...how-dialog-statistics-fix-units-changed.ts | 21 ++ src/translations/en.json | 17 ++ 8 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/panels/developer-tools/statistics/developer-tools-statistics.ts create mode 100644 src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts create mode 100644 src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index c1a69b8858..2209d54f59 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -550,7 +550,7 @@ export class HaDataTable extends LitElement { private _handleRowClick(ev: Event) { const target = ev.target as HTMLElement; - if (target.tagName === "HA-CHECKBOX") { + if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) { return; } const rowId = (ev.currentTarget as any).rowId; diff --git a/src/data/history.ts b/src/data/history.ts index 4e55ab711d..0a225f2685 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -76,6 +76,23 @@ export interface StatisticsMetaData { statistic_id: string; } +export type StatisticsValidationResult = + | StatisticsValidationResultUnsupportedUnit + | StatisticsValidationResultUnitsChanged; + +export interface StatisticsValidationResultUnsupportedUnit { + type: "unsupported_unit"; + data: { statistic_id: string; device_class: string; state_unit: string }; +} + +export interface StatisticsValidationResultUnitsChanged { + type: "units_changed"; + data: { statistic_id: string; state_unit: string; metadata_unit: string }; +} +export interface StatisticsValidationResults { + [statisticId: string]: StatisticsValidationResult[]; +} + export const fetchRecent = ( hass: HomeAssistant, entityId: string, @@ -305,6 +322,28 @@ export const fetchStatistics = ( period, }); +export const validateStatistics = (hass: HomeAssistant) => + hass.callWS({ + type: "recorder/validate_statistics", + }); + +export const updateStatisticsMetadata = ( + hass: HomeAssistant, + statistic_id: string, + unit_of_measurement: string | null +) => + hass.callWS({ + type: "recorder/update_statistics_metadata", + statistic_id, + unit_of_measurement, + }); + +export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) => + hass.callWS({ + type: "recorder/clear_statistics", + statistic_ids, + }); + export const calculateStatisticSumGrowth = ( values: StatisticValue[] ): number | null => { diff --git a/src/panels/developer-tools/developer-tools-router.ts b/src/panels/developer-tools/developer-tools-router.ts index efc0c01ddc..308a1cc348 100644 --- a/src/panels/developer-tools/developer-tools-router.ts +++ b/src/panels/developer-tools/developer-tools-router.ts @@ -37,6 +37,10 @@ class DeveloperToolsRouter extends HassRouterPage { tag: "developer-tools-template", load: () => import("./template/developer-tools-template"), }, + statistics: { + tag: "developer-tools-statistics", + load: () => import("./statistics/developer-tools-statistics"), + }, }, }; diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts index 3d05125563..c8c4609515 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -63,6 +63,11 @@ class PanelDeveloperTools extends LitElement { "ui.panel.developer-tools.tabs.events.title" )} + + ${this.hass.localize( + "ui.panel.developer-tools.tabs.statistics.title" + )} + + html`${entityState + ? computeStateName(entityState) + : data.statistic_id}`, + }, + statistic_id: { + title: "Statistic id", + sortable: true, + filterable: true, + hidden: this.narrow, + width: "30%", + }, + unit_of_measurement: { + title: "Unit", + sortable: true, + filterable: true, + width: "10%", + }, + issues: { + title: "Issue", + sortable: true, + filterable: true, + direction: "asc", + width: "30%", + template: (issues) => + html`${issues + ? issues.map( + (issue) => + this.hass.localize( + `ui.panel.developer-tools.tabs.statistics.issues.${issue.type}`, + issue.data + ) || issue.type + ) + : ""}`, + }, + fix: { + title: "", + template: (_, data: any) => + html`${data.issues + ? html`Fix issue` + : ""}`, + width: "113px", + }, + }; + + protected render() { + return html` + + `; + } + + private _rowClicked(ev) { + const id = ev.detail.id; + if (id in this.hass.states) { + fireEvent(this, "hass-more-info", { entityId: id }); + } + } + + private async _validateStatistics() { + const [statisticIds, issues] = await Promise.all([ + getStatisticIds(this.hass), + validateStatistics(this.hass), + ]); + + this._data = statisticIds.map((statistic) => ({ + ...statistic, + state: this.hass.states[statistic.statistic_id], + issues: issues[statistic.statistic_id], + })); + } + + private _fixIssue(ev) { + const issue = ev.currentTarget.data[0] as StatisticsValidationResult; + if (issue.type === "unsupported_unit") { + showAlertDialog(this, { + title: "Unsupported unit", + text: html`The unit of your entity is not a suppported unit for the + device class of the entity, ${issue.data.device_class}. +
Statistics can not be generated until this entity has a + supported unit.

If this unit was provided by an + integration, this is a bug. Please report an issue.

If you + have set this unit yourself, and want to have statistics generated, + make sure the unit matched the device class. The supported units are + documented in the + + developer documentation.`, + }); + return; + } + if (issue.type === "units_changed") { + showFixStatisticsUnitsChangedDialog(this, { + issue, + fixedCallback: () => { + this._validateStatistics(); + }, + }); + return; + } + showAlertDialog(this, { + title: "Fix issue", + text: "Fixing this issue is not supported yet.", + }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .content { + padding: 16px; + } + + th { + padding: 0 8px; + text-align: left; + font-size: var( + --paper-input-container-shared-input-style_-_font-size + ); + } + + :host([rtl]) th { + text-align: right; + } + + tr { + vertical-align: top; + direction: ltr; + } + + tr:nth-child(odd) { + background-color: var(--table-row-background-color, #fff); + } + + tr:nth-child(even) { + background-color: var(--table-row-alternative-background-color, #eee); + } + td { + padding: 4px; + min-width: 200px; + word-break: break-word; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-statistics": HaPanelDevStatistics; + } +} diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts new file mode 100644 index 0000000000..671fc81177 --- /dev/null +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -0,0 +1,122 @@ +import "@material/mwc-button/mwc-button"; +import { LitElement, TemplateResult, html, CSSResultGroup } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-dialog"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { + clearStatistics, + updateStatisticsMetadata, +} from "../../../data/history"; +import "../../../components/ha-formfield"; +import "../../../components/ha-radio"; +import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; + +@customElement("dialog-statistics-fix-units-changed") +export class DialogStatisticsFixUnitsChanged extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: DialogStatisticsUnitsChangedParams; + + @state() private _action?: "update" | "clear"; + + public showDialog(params: DialogStatisticsUnitsChangedParams): void { + this._params = params; + this._action = "update"; + } + + public closeDialog(): void { + this._params = undefined; + this._action = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + + return html` + +

+ The unit of this entity changed, we can't store values in multiple + units.
If the historic statistic values have a wrong unit, you + can update the units of the old values. The values will not be + updated.
Otherwise you can choose to delete all historic + statistic values, and start over. +

+ +

How do you want to fix this issue?

+ + + + + + + + + ${this.hass.localize( + "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.fix" + )} + + + ${this.hass.localize("ui.common.close")} + +
+ `; + } + + private _handleActionChanged(ev): void { + this._action = ev.target.value; + } + + private async _fixIssue(): Promise { + if (this._action === "clear") { + await clearStatistics(this.hass, [this._params!.issue.data.statistic_id]); + } else { + await updateStatisticsMetadata( + this.hass, + this._params!.issue.data.statistic_id, + this._params!.issue.data.state_unit + ); + } + this._params?.fixedCallback(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [haStyle, haStyleDialog]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-statistics-fix-units-changed": DialogStatisticsFixUnitsChanged; + } +} diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts new file mode 100644 index 0000000000..c341bfdf21 --- /dev/null +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { StatisticsValidationResultUnitsChanged } from "../../../data/history"; + +export const loadFixUnitsDialog = () => + import("./dialog-statistics-fix-units-changed"); + +export interface DialogStatisticsUnitsChangedParams { + issue: StatisticsValidationResultUnitsChanged; + fixedCallback: () => void; +} + +export const showFixStatisticsUnitsChangedDialog = ( + element: HTMLElement, + detailParams: DialogStatisticsUnitsChangedParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-statistics-fix-units-changed", + dialogImport: loadFixUnitsDialog, + dialogParams: detailParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index a1c51b98d2..2e93ab5ab6 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3732,6 +3732,23 @@ "listeners": "This template listens for the following state changed events:", "entity": "Entity", "domain": "Domain" + }, + "statistics": { + "title": "Statistics", + "entity": "Entity", + "issue": "Issue", + "issues": { + "units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.", + "unsupported_unit": "The unit (''{state_unit}'') of this entity doesn't match a unit of device class ''{device_class}''." + }, + "fix_issue": { + "units_changed": { + "title": "The unit of this entity changed", + "update": "Update the historic statistic values from ''{metadata_unit}'' to ''{state_unit}''", + "clear": "Delete all old statistic data for this entity", + "fix": "Fix issue" + } + } } } },