Allow to fix statistic issue from repairs (#22055)

* Allow to fix statistic issue from repairs

* clean up, add names

* Update src/translations/en.json

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* address review

* Update src/translations/en.json

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Bram Kragten 2024-09-25 15:55:49 +02:00 committed by GitHub
parent 7462f8fbe3
commit 765812331b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 276 additions and 177 deletions

View File

@ -47,6 +47,14 @@ export interface StatisticsMetaData {
unit_class: string | null;
}
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
"entity_not_recorded",
"entity_no_longer_recorded",
"unsupported_state_class",
"units_changed",
"no_state",
];
export type StatisticsValidationResult =
| StatisticsValidationResultNoState
| StatisticsValidationResultEntityNotRecorded

View File

@ -12,11 +12,16 @@ import {
fetchRepairsIssueData,
type RepairsIssue,
} from "../../../data/repairs";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { fixStatisticsIssue } from "../../developer-tools/statistics/fix-statistics";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
STATISTIC_TYPES,
StatisticsValidationResult,
} from "../../../data/recorder";
@customElement("ha-config-repairs")
class HaConfigRepairs extends LitElement {
@ -130,6 +135,31 @@ class HaConfigRepairs extends LitElement {
continueFlowId: data.issue_data.flow_id as string,
});
}
} else if (
issue.domain === "sensor" &&
issue.translation_key &&
STATISTIC_TYPES.includes(issue.translation_key as any)
) {
const localize =
await this.hass.loadFragmentTranslation("developer-tools");
const data = await fetchRepairsIssueData(
this.hass.connection,
issue.domain,
issue.issue_id
);
if ("issue_type" in data.issue_data) {
await fixStatisticsIssue(
this,
this.hass,
localize || this.hass.localize,
{
type: data.issue_data
.issue_type as StatisticsValidationResult["type"],
data: data.issue_data as any,
}
);
this.hass.callWS({ type: "recorder/update_statistics_issues" });
}
} else {
showRepairsIssueDialog(this, {
issue,

View File

@ -5,28 +5,22 @@ import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
clearStatistics,
getStatisticIds,
StatisticsMetaData,
StatisticsValidationResult,
validateStatistics,
} from "../../../data/recorder";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { documentationUrl } from "../../../util/documentation-url";
const FIX_ISSUES_ORDER = {
no_state: 0,
@ -264,162 +258,30 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
});
}
private _fixIssue = (ev) => {
private _fixIssue = async (ev) => {
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
(itemA, itemB) =>
(FIX_ISSUES_ORDER[itemA.type] ?? 99) -
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
);
const issue = issues[0];
switch (issue.type) {
case "no_state":
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_1"
)}<br /><br />${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_2",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
this._validateStatistics();
},
});
break;
case "entity_not_recorded":
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_1"
)}<br /><br />${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_2"
)}<br /><br />
<a
href=${documentationUrl(
this.hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`,
});
break;
case "entity_no_longer_recorded":
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_1"
)}
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_2"
)}
<a
href=${documentationUrl(
this.hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
this._validateStatistics();
},
});
break;
case "unsupported_state_class":
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_1",
{ state_class: issue.data.state_class }
)}<br /><br />
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_2"
)}
<ul>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_3"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4_link"
)}</a
>
</li>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_5"
)}
</li>
</ul>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
this._validateStatistics();
},
});
break;
case "units_changed":
showFixStatisticsUnitsChangedDialog(this, {
issue,
fixedCallback: () => {
this._validateStatistics();
},
});
break;
default:
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.title"
),
text: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.info_text_1"
),
});
const result = await fixStatisticsIssue(
this,
this.hass,
this.hass.localize,
issue
);
if (
result &&
[
"no_state",
"entity_no_longer_recorded",
"unsupported_state_class",
].includes(issue.type)
) {
this._deletedStatistics.add(issue.data.statistic_id);
}
this._validateStatistics();
};
static get styles(): CSSResultGroup {

View File

@ -7,6 +7,7 @@ import "../../../components/ha-formfield";
import "../../../components/ha-radio";
import {
clearStatistics,
getStatisticLabel,
updateStatisticsMetadata,
} from "../../../data/recorder";
import { haStyle, haStyleDialog } from "../../../resources/styles";
@ -27,6 +28,10 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
}
public closeDialog(): void {
this._cancel();
}
private _closeDialog(): void {
this._params = undefined;
this._action = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@ -40,7 +45,9 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
@closed=${this._closeDialog}
.heading=${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.title"
)}
@ -49,6 +56,11 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_1",
{
name: getStatisticLabel(
this.hass,
this._params.issue.data.statistic_id,
undefined
),
current_unit: this._params.issue.data.state_unit,
previous_unit: this._params.issue.data.metadata_unit,
}
@ -98,7 +110,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
)}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
<mwc-button slot="secondaryAction" @click=${this._cancel}>
${this.hass.localize("ui.common.close")}
</mwc-button>
</ha-dialog>
@ -109,6 +121,11 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
this._action = ev.target.value;
}
private _cancel(): void {
this._params?.cancelCallback!();
this._closeDialog();
}
private async _fixIssue(): Promise<void> {
if (this._action === "clear") {
await clearStatistics(this.hass, [this._params!.issue.data.statistic_id]);
@ -119,8 +136,8 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
this._params!.issue.data.state_unit
);
}
this._params?.fixedCallback();
this.closeDialog();
this._params?.fixedCallback!();
this._closeDialog();
}
static get styles(): CSSResultGroup {

View File

@ -0,0 +1,169 @@
import { html } from "lit";
import {
clearStatistics,
getStatisticLabel,
StatisticsValidationResult,
} from "../../../data/recorder";
import { documentationUrl } from "../../../util/documentation-url";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../lovelace/custom-card-helpers";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { LocalizeFunc } from "../../../common/translations/localize";
import { HomeAssistant } from "../../../types";
export const fixStatisticsIssue = async (
element: HTMLElement,
hass: HomeAssistant,
localize: LocalizeFunc,
issue: StatisticsValidationResult
) => {
switch (issue.type) {
case "no_state":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
}
)}<br /><br />${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_2",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "entity_not_recorded":
return showAlertDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
}
)}<br /><br />${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_2"
)}<br /><br />
<a
href=${documentationUrl(
hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`,
});
case "entity_no_longer_recorded":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
}
)}
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_2"
)}
<a
href=${documentationUrl(
hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "unsupported_state_class":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
state_class: issue.data.state_class,
}
)}<br /><br />
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_2"
)}
<ul>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_3"
)}
</li>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4_link"
)}</a
>
</li>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_5"
)}
</li>
</ul>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "units_changed":
return showFixStatisticsUnitsChangedDialog(element, {
issue,
});
default:
return showAlertDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.title"
),
text: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.info_text_1"
),
});
}
};

View File

@ -6,16 +6,29 @@ export const loadFixUnitsDialog = () =>
export interface DialogStatisticsUnitsChangedParams {
issue: StatisticsValidationResultUnitsChanged;
fixedCallback: () => void;
fixedCallback?: () => void;
cancelCallback?: () => void;
}
export const showFixStatisticsUnitsChangedDialog = (
element: HTMLElement,
detailParams: DialogStatisticsUnitsChangedParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-statistics-fix-units-changed",
dialogImport: loadFixUnitsDialog,
dialogParams: detailParams,
) =>
new Promise((resolve) => {
const origCallback = detailParams.fixedCallback;
fireEvent(element, "show-dialog", {
dialogTag: "dialog-statistics-fix-units-changed",
dialogImport: loadFixUnitsDialog,
dialogParams: {
...detailParams,
cancelCallback: () => {
resolve(false);
},
fixedCallback: () => {
resolve(true);
origCallback?.();
},
},
});
});
};

View File

@ -6958,25 +6958,25 @@
},
"no_state": {
"title": "Entity has no state",
"info_text_1": "This entity has no state at the moment, if this is an orphaned entity, you may want to delete the long term statistics of it from your database.",
"info_text_1": "{name} has no state at the moment, if this is an orphaned entity, you may want to delete the long term statistics of it from your database.",
"info_text_2": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
},
"entity_not_recorded": {
"title": "Entity not recorded",
"info_text_1": "State changes of this entity are not recorded, therefore, we cannot track long term statistics for it.",
"title": "Entity is not recorded",
"info_text_1": "State changes of {name} are not recorded, therefore, we cannot track long term statistics for it.",
"info_text_2": "You probably excluded this entity, or have just included some entities.",
"info_text_3_link": "See the recorder documentation for more information."
},
"entity_no_longer_recorded": {
"title": "Entity no longer recorded",
"info_text_1": "We have generated statistics for this entity in the past, but state changes of this entity are no longer recorded, therefore, we cannot track long term statistics for it anymore.",
"title": "Entity is no longer recorded",
"info_text_1": "We have generated statistics for {name} in the past, but state changes of this entity are no longer recorded, therefore, we cannot track long term statistics for it anymore.",
"info_text_2": "You probably excluded this entity, or have just included some entities.",
"info_text_3_link": "See the recorder documentation for more information.",
"info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now."
},
"unsupported_state_class": {
"title": "Unsupported state class",
"info_text_1": "The state class of this entity, {state_class} is not supported.",
"info_text_1": "The state class of {name}, {state_class} is not supported.",
"info_text_2": "Statistics cannot be generated until this entity has a supported state class.",
"info_text_3": "If this state class was provided by an integration, this is a bug. Please report an issue.",
"info_text_4": "If you have set this state class yourself, please correct it.",
@ -6985,11 +6985,11 @@
"info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
},
"units_changed": {
"title": "The unit of this entity changed",
"title": "The unit changed",
"update": "Update the unit of the historic statistic values from ''{metadata_unit}'' to ''{state_unit}'', without converting.",
"clear": "Delete all old statistic data for this entity",
"how_to_fix": "How do you want to fix this issue?",
"info_text_1": "The unit of this entity changed to ''{current_unit}'' which can't be converted to the previously stored unit, ''{previous_unit}''.",
"info_text_1": "The unit of {name} changed to ''{current_unit}'' which can't be converted to the previously stored unit, ''{previous_unit}''.",
"info_text_2": "If the historic statistic values have a wrong unit, you can update the units of the old values. The values will not be updated.",
"info_text_3": "Otherwise you can choose to delete all historic statistic values, and start over."
},