mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-30 17:43:39 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a88453db | ||
|
|
fba2451e61 | ||
|
|
c33a452976 | ||
|
|
ec0533f8c5 | ||
|
|
b1b9c41bc1 | ||
|
|
685e7d51d2 | ||
|
|
315afd8ac2 | ||
|
|
bc9d0b5227 | ||
|
|
99c85c3407 | ||
|
|
e04e639100 | ||
|
|
ca7b91af35 | ||
|
|
ffd26ffc1a | ||
|
|
bd4ff71d29 |
No files matched your search
+1
-1
@@ -110,7 +110,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"marked": "18.1.2",
|
||||
"marked": "18.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
|
||||
Generated
+5
-5
@@ -391,8 +391,8 @@ importers:
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
marked:
|
||||
specifier: 18.1.2
|
||||
version: 18.1.2
|
||||
specifier: 18.1.1
|
||||
version: 18.1.1
|
||||
memoize-one:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -5907,8 +5907,8 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-DfOnvLAQ6WYeWxWO1u4RyFOeqyHup6dks5PPqnoFiooU7ipRvidO/2j8plhAHIiZg574E9ZlzSjUiPMLAzFkgg==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
@@ -13689,7 +13689,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].2: {}
|
||||
[email protected].1: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
@@ -32,8 +31,8 @@ import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
import "../ha-icon-button-prev";
|
||||
import "../item/ha-list-item-button";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
@@ -166,19 +165,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-list-item-button
|
||||
.range=${range}
|
||||
.index=${index}
|
||||
@click=${this._setDateRange}
|
||||
>
|
||||
<span slot="headline">${name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -351,12 +342,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { range: [Date, Date]; index: number }
|
||||
>
|
||||
) {
|
||||
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
|
||||
private _setDateRange(ev: HASSDomEvent<{ index: number }>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
|
||||
@@ -107,6 +107,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public ranges?: DateRangePickerRanges | false;
|
||||
|
||||
// Added after the default presets
|
||||
@property({ attribute: false }) public extraRanges?: DateRangePickerRanges;
|
||||
|
||||
@state() private _ranges?: DateRangePickerRanges;
|
||||
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
@@ -166,7 +169,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
if (
|
||||
changedProps.has("_i18n") ||
|
||||
changedProps.has("_hassConfig") ||
|
||||
changedProps.has("extendedPresets")
|
||||
changedProps.has("extendedPresets") ||
|
||||
changedProps.has("extraRanges")
|
||||
) {
|
||||
const rangeKeys = this.extendedPresets
|
||||
? [...RANGE_KEYS, ...EXTENDED_RANGE_KEYS]
|
||||
@@ -178,7 +182,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
this._i18n.localize(`ui.components.date-range-picker.ranges.${key}`)
|
||||
] = calcDateRange(this._i18n.locale, this._hassConfig, key);
|
||||
});
|
||||
this._ranges = ranges;
|
||||
this._ranges = { ...ranges, ...this.extraRanges };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ export const statisticsMetaHasType = (
|
||||
};
|
||||
|
||||
export const adjustStatisticsSum = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
statistic_id: string,
|
||||
start_time: number,
|
||||
adjustment: number,
|
||||
|
||||
@@ -5,10 +5,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/user/ha-person-badge";
|
||||
import type { Person } from "../../../data/person";
|
||||
import {
|
||||
@@ -93,22 +92,23 @@ export class HaConfigPerson extends LitElement {
|
||||
</span>
|
||||
|
||||
<ha-card outlined class="storage">
|
||||
<ha-list-base>
|
||||
<ha-list>
|
||||
${this._storageItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item-button
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
@click=${this._openEditEntry}
|
||||
.entry=${entry}
|
||||
>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-button>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
${
|
||||
this._storageItems.length === 0
|
||||
? html`
|
||||
@@ -138,19 +138,19 @@ export class HaConfigPerson extends LitElement {
|
||||
this._configItems.length > 0
|
||||
? html`
|
||||
<ha-card outlined header="Configuration.yaml persons">
|
||||
<ha-list-base>
|
||||
<ha-list>
|
||||
${this._configItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item graphic="avatar">
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-base>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing
|
||||
@@ -299,7 +299,7 @@ export class HaConfigPerson extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
ha-list-base:has(+ .empty) {
|
||||
ha-list:has(+ .empty) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,665 +0,0 @@
|
||||
import { formatISO9075 } from "date-fns";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { formatNumberWithUnit } from "../../../../common/number/format_number";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-selector/ha-selector-datetime";
|
||||
import "../../../../components/ha-selector/ha-selector-number";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { StatisticValue } from "../../../../data/recorder";
|
||||
import {
|
||||
adjustStatisticsSum,
|
||||
fetchStatistics,
|
||||
getDisplayUnit,
|
||||
} from "../../../../data/recorder";
|
||||
import type {
|
||||
DateTimeSelector,
|
||||
NumberSelector,
|
||||
} from "../../../../data/selector";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
interface CombinedStat {
|
||||
hour: StatisticValue | null;
|
||||
fiveMin: StatisticValue[];
|
||||
}
|
||||
|
||||
interface FetchedStats {
|
||||
hour: StatisticValue[];
|
||||
fiveMin: StatisticValue[];
|
||||
}
|
||||
|
||||
interface AdjustState {
|
||||
amount: number | undefined;
|
||||
}
|
||||
|
||||
@customElement("dialog-statistics-adjust-sum")
|
||||
export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProviderMixin<AdjustState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DialogStatisticsAdjustSumParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _busy = false;
|
||||
|
||||
@state() private _moment?: string;
|
||||
|
||||
@state() private _stats5min?: StatisticValue[];
|
||||
|
||||
@state() private _statsHour?: StatisticValue[];
|
||||
|
||||
@state() private _chosenStat?: StatisticValue;
|
||||
|
||||
@state() private _amount?: number;
|
||||
|
||||
private _showingOutliers = false;
|
||||
|
||||
// Adjustments made while the dialog is open, keyed by period. The recorder
|
||||
// processes adjustments asynchronously, so a fetch right after adjusting can
|
||||
// still return the old values.
|
||||
private _adjustedChanges = new Map<string, number>();
|
||||
|
||||
// Incremented for every fetch, so results of outdated fetches are ignored
|
||||
private _fetchId = 0;
|
||||
|
||||
private _dateTimeSelector: DateTimeSelector = {
|
||||
datetime: {},
|
||||
};
|
||||
|
||||
private _precision = 2;
|
||||
|
||||
private _amountSelector = memoizeOne(
|
||||
(
|
||||
unit_of_measurement: string | undefined,
|
||||
precision: number
|
||||
): NumberSelector => ({
|
||||
number: {
|
||||
step: 10 ** -precision,
|
||||
unit_of_measurement,
|
||||
mode: "box",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
public showDialog(params: DialogStatisticsAdjustSumParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
// moment is in format YYYY-MM-DD HH:mm:ss because of selector
|
||||
// Here we create a date with minutes set to %5
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() - (now.getMinutes() % 5), 0);
|
||||
this._moment = formatISO9075(now);
|
||||
this._showingOutliers = false;
|
||||
this._adjustedChanges.clear();
|
||||
this._refreshStats();
|
||||
|
||||
const entry = this.hass.entities[params.statistic.statistic_id];
|
||||
this._precision = Math.max(entry?.display_precision ?? 0, 2);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._moment = undefined;
|
||||
this._stats5min = undefined;
|
||||
this._statsHour = undefined;
|
||||
this._amount = undefined;
|
||||
this._chosenStat = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, { amount: undefined });
|
||||
this._busy = false;
|
||||
this._showingOutliers = false;
|
||||
this._adjustedChanges.clear();
|
||||
this._fetchId++;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let content: TemplateResult;
|
||||
let footer: TemplateResult;
|
||||
|
||||
if (!this._chosenStat) {
|
||||
content = this._renderPickStatistic();
|
||||
footer = html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._showOutliers}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.outliers"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" data-dialog="close">
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
`;
|
||||
} else {
|
||||
content = this._renderAdjustStat();
|
||||
footer = html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
.disabled=${this._busy}
|
||||
@click=${this._clearChosenStatistic}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.common.back")}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._busy || !this.isDirtyState}
|
||||
@click=${this._fixIssue}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.adjust"
|
||||
)}</ha-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.title"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${content}
|
||||
<ha-dialog-footer slot="footer">${footer}</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
|
||||
if (changedProps.size !== 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
// We only respond to hass changes if the translations or locale changed
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
return (
|
||||
!oldHass ||
|
||||
oldHass.localize !== this.hass.localize ||
|
||||
oldHass.locale !== this.hass.locale
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPickStatistic() {
|
||||
let stats: TemplateResult;
|
||||
|
||||
if (!this._stats5min || !this._statsHour) {
|
||||
stats = html`<ha-spinner></ha-spinner>`;
|
||||
} else if (this._statsHour.length < 1 && this._stats5min.length < 1) {
|
||||
stats = html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found"
|
||||
)}
|
||||
</p>`;
|
||||
} else {
|
||||
const data =
|
||||
this._stats5min.length >= 1 ? this._stats5min : this._statsHour;
|
||||
const unit = getDisplayUnit(
|
||||
this.hass.states,
|
||||
this._params!.statistic.statistic_id,
|
||||
this._params!.statistic
|
||||
);
|
||||
const rows: TemplateResult[] = [];
|
||||
for (const stat of data) {
|
||||
const digits = this._fractionDigits(stat.change);
|
||||
const change = formatNumberWithUnit(
|
||||
stat.change!,
|
||||
unit,
|
||||
this.hass.locale,
|
||||
{
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}
|
||||
);
|
||||
rows.push(html`
|
||||
<ha-list-item
|
||||
twoline
|
||||
hasMeta
|
||||
.stat=${stat}
|
||||
@click=${this._setChosenStatistic}
|
||||
>
|
||||
<span>${change}</span>
|
||||
<span slot="secondary">
|
||||
${formatDateTime(
|
||||
new Date(stat.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`);
|
||||
}
|
||||
stats = html`${rows}`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="text-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.info_text_1"
|
||||
)}
|
||||
</div>
|
||||
<div class="text-content">
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
|
||||
)}</b
|
||||
>
|
||||
${this._params!.statistic.statistic_id}
|
||||
</div>
|
||||
<ha-selector-datetime
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.pick_a_time"
|
||||
)}
|
||||
.selector=${this._dateTimeSelector}
|
||||
.value=${this._moment}
|
||||
@value-changed=${this._dateTimeSelectorChanged}
|
||||
></ha-selector-datetime>
|
||||
<div class="stat-list">${stats}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearChosenStatistic() {
|
||||
this._chosenStat = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, { amount: undefined });
|
||||
}
|
||||
|
||||
// Small changes get more digits, so they do not round to zero.
|
||||
private _fractionDigits(value: number | null | undefined): number {
|
||||
if (!value || Math.abs(value) >= 10 ** -this._precision) {
|
||||
return this._precision;
|
||||
}
|
||||
return Math.min(Math.ceil(-Math.log10(Math.abs(value))) + 1, 20);
|
||||
}
|
||||
|
||||
private _setChosenStatistic(ev) {
|
||||
const stat = ev.currentTarget.stat;
|
||||
const multiple = 10 ** this._fractionDigits(stat.change);
|
||||
const growth = Math.round(stat.change! * multiple) / multiple;
|
||||
|
||||
this._chosenStat = stat;
|
||||
this._amount = growth;
|
||||
this._initDirtyTracking({ type: "deep" }, { amount: growth });
|
||||
}
|
||||
|
||||
private _dateTimeSelectorChanged(ev) {
|
||||
this._moment = ev.detail.value;
|
||||
this._showingOutliers = false;
|
||||
this._refreshStats();
|
||||
}
|
||||
|
||||
private _showOutliers() {
|
||||
this._showingOutliers = true;
|
||||
this._refreshStats();
|
||||
}
|
||||
|
||||
private async _refreshStats(): Promise<void> {
|
||||
const fetchId = ++this._fetchId;
|
||||
this._stats5min = undefined;
|
||||
this._statsHour = undefined;
|
||||
try {
|
||||
const stats = this._showingOutliers
|
||||
? await this._fetchOutliers()
|
||||
: await this._fetchStats();
|
||||
if (fetchId !== this._fetchId) {
|
||||
return;
|
||||
}
|
||||
this._statsHour = stats.hour;
|
||||
this._stats5min = stats.fiveMin;
|
||||
} catch (err: any) {
|
||||
if (fetchId !== this._fetchId) {
|
||||
return;
|
||||
}
|
||||
this._stats5min = [];
|
||||
this._statsHour = [];
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.error_loading_statistics",
|
||||
{ message: err.message || err }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _applyAdjustedChanges(stats: StatisticValue[]): StatisticValue[] {
|
||||
if (!this._adjustedChanges.size) {
|
||||
return stats;
|
||||
}
|
||||
return stats.map((stat) => {
|
||||
const change = this._adjustedChanges.get(`${stat.start}-${stat.end}`);
|
||||
return change === undefined ? stat : { ...stat, change };
|
||||
});
|
||||
}
|
||||
|
||||
private _renderAdjustStat() {
|
||||
const unit = getDisplayUnit(
|
||||
this.hass.states,
|
||||
this._params!.statistic.statistic_id,
|
||||
this._params!.statistic
|
||||
);
|
||||
return html`
|
||||
<div class="text-content">
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
|
||||
)}</b
|
||||
>
|
||||
${this._params!.statistic.statistic_id}
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.start"
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
>${formatDateTime(
|
||||
new Date(this._chosenStat!.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.end"
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
>${formatDateTime(
|
||||
new Date(this._chosenStat!.end),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-selector-number
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.new_value"
|
||||
)}
|
||||
.selector=${this._amountSelector(
|
||||
unit || undefined,
|
||||
this._fractionDigits(this._chosenStat!.change)
|
||||
)}
|
||||
.value=${this._amount}
|
||||
.disabled=${this._busy}
|
||||
@value-changed=${this._amountChanged}
|
||||
></ha-selector-number>
|
||||
`;
|
||||
}
|
||||
|
||||
private _amountChanged(ev) {
|
||||
this._amount = ev.detail.value;
|
||||
this._updateDirtyState({ amount: this._amount });
|
||||
}
|
||||
|
||||
private async _fetchStats(): Promise<FetchedStats> {
|
||||
const statId = this._params!.statistic.statistic_id;
|
||||
|
||||
// moment is in format YYYY-MM-DD HH:mm:ss because of selector
|
||||
// Here we convert it to an ISO string.
|
||||
const moment = new Date(this._moment!.replace(" ", "T"));
|
||||
|
||||
// Search 2 hours before and 3 hours after chosen time
|
||||
const hourStatStart = new Date(moment.getTime());
|
||||
hourStatStart.setTime(hourStatStart.getTime() - 2 * 3600 * 1000);
|
||||
const hourStatEnd = new Date(moment.getTime());
|
||||
hourStatEnd.setTime(hourStatEnd.getTime() + 3 * 3600 * 1000);
|
||||
|
||||
const statsHourData = await fetchStatistics(
|
||||
this.hass.callWS,
|
||||
hourStatStart,
|
||||
hourStatEnd,
|
||||
[statId],
|
||||
"hour"
|
||||
);
|
||||
const statsHour =
|
||||
statId in statsHourData
|
||||
? this._applyAdjustedChanges(statsHourData[statId].slice(0, 5))
|
||||
: [];
|
||||
|
||||
// Can't have 5 min data if no hourly data
|
||||
if (statsHour.length === 0) {
|
||||
return { hour: [], fiveMin: [] };
|
||||
}
|
||||
|
||||
// Search 10 minutes before and 15 minutes after chosen time
|
||||
const minStatStart = new Date(moment.getTime());
|
||||
minStatStart.setTime(minStatStart.getTime() - 10 * 60 * 1000);
|
||||
const minStatEnd = new Date(moment.getTime());
|
||||
minStatEnd.setTime(minStatEnd.getTime() + 15 * 60 * 1000);
|
||||
|
||||
const stats5MinData = await fetchStatistics(
|
||||
this.hass.callWS,
|
||||
minStatStart,
|
||||
minStatEnd,
|
||||
[statId],
|
||||
"5minute"
|
||||
);
|
||||
|
||||
return {
|
||||
hour: statsHour,
|
||||
fiveMin:
|
||||
statId in stats5MinData
|
||||
? this._applyAdjustedChanges(stats5MinData[statId].slice(0, 5))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async _fetchOutliers(): Promise<FetchedStats> {
|
||||
const statId = this._params!.statistic.statistic_id;
|
||||
|
||||
// Get all the data
|
||||
const start = new Date(0);
|
||||
const end = new Date();
|
||||
|
||||
const statsHourData = await fetchStatistics(
|
||||
this.hass.callWS,
|
||||
start,
|
||||
end,
|
||||
[statId],
|
||||
"hour"
|
||||
);
|
||||
|
||||
const statsHour =
|
||||
statId in statsHourData
|
||||
? this._applyAdjustedChanges(statsHourData[statId])
|
||||
: [];
|
||||
if (statsHour.length === 0) {
|
||||
return { hour: [], fiveMin: [] };
|
||||
}
|
||||
|
||||
const stats5MinData = await fetchStatistics(
|
||||
this.hass.callWS,
|
||||
start,
|
||||
end,
|
||||
[statId],
|
||||
"5minute"
|
||||
);
|
||||
|
||||
const stats5Min =
|
||||
statId in stats5MinData
|
||||
? this._applyAdjustedChanges(stats5MinData[statId])
|
||||
: [];
|
||||
// First datapoint of 5 minute data in the history is always junk since it counts the entire sum
|
||||
// as the change, which we don't want here.
|
||||
stats5Min.shift();
|
||||
|
||||
const combinedStatsData: CombinedStat[] = [];
|
||||
statsHour.forEach((s) => {
|
||||
combinedStatsData.push({ hour: s, fiveMin: [] });
|
||||
});
|
||||
|
||||
const lasthour: CombinedStat = { hour: null, fiveMin: [] };
|
||||
|
||||
let i = 0;
|
||||
stats5Min.forEach((s) => {
|
||||
let matched = false;
|
||||
for (i; i < combinedStatsData.length; i++) {
|
||||
const hour = combinedStatsData[i].hour;
|
||||
if (hour && s.start >= hour.start && s.end <= hour.end) {
|
||||
combinedStatsData[i].fiveMin.push(s);
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
lasthour.fiveMin.push(s);
|
||||
}
|
||||
});
|
||||
|
||||
combinedStatsData.push(lasthour);
|
||||
|
||||
let statsOutliers: StatisticValue[] = [];
|
||||
let min = 0;
|
||||
const numOutliers = 10;
|
||||
|
||||
// Track the top 10 values.
|
||||
const addOutlier = (s) => {
|
||||
const val = Math.abs(s.change ?? 0);
|
||||
if (statsOutliers.length < numOutliers || val > min) {
|
||||
statsOutliers.push(s);
|
||||
statsOutliers = statsOutliers.sort(
|
||||
(a, b) => Math.abs(b.change ?? 0) - Math.abs(a.change ?? 0)
|
||||
);
|
||||
statsOutliers = statsOutliers.slice(0, numOutliers);
|
||||
min = statsOutliers[statsOutliers.length - 1].change ?? 0;
|
||||
}
|
||||
};
|
||||
|
||||
// If an hour has incomplete five minute data, add the hour value
|
||||
// Otherwise, add the 5 minute values and ignore the hour value
|
||||
combinedStatsData.forEach((c) => {
|
||||
if (c.fiveMin.length !== 12 && c.hour) {
|
||||
addOutlier(c.hour);
|
||||
} else {
|
||||
c.fiveMin.forEach((s) => {
|
||||
addOutlier(s);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Outliers are a possible mix of hour/5minute data, but the distinction
|
||||
// is not relevant here, as long as only one array is populated.
|
||||
return { hour: statsOutliers, fiveMin: [] };
|
||||
}
|
||||
|
||||
private async _fixIssue(): Promise<void> {
|
||||
const params = this._params!;
|
||||
const chosenStat = this._chosenStat!;
|
||||
const amount = this._amount!;
|
||||
const unit = getDisplayUnit(
|
||||
this.hass.states,
|
||||
params.statistic.statistic_id,
|
||||
params.statistic
|
||||
);
|
||||
this._busy = true;
|
||||
try {
|
||||
await adjustStatisticsSum(
|
||||
this.hass,
|
||||
params.statistic.statistic_id,
|
||||
chosenStat.start,
|
||||
// The shown value is rounded, so compare with the stored value.
|
||||
amount - chosenStat.change!,
|
||||
unit || null
|
||||
);
|
||||
} catch (err: any) {
|
||||
// Report the error even if the dialog was closed or reopened while
|
||||
// adjusting, but leave the state of a newer invocation alone
|
||||
if (this._params === params) {
|
||||
this._busy = false;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted",
|
||||
{ message: err.message || err }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted"
|
||||
),
|
||||
});
|
||||
// The dialog was closed or reopened while adjusting
|
||||
if (this._params !== params) {
|
||||
return;
|
||||
}
|
||||
this._adjustedChanges.set(`${chosenStat.start}-${chosenStat.end}`, amount);
|
||||
this._busy = false;
|
||||
this._clearChosenStatistic();
|
||||
this._refreshStats();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.text-content,
|
||||
ha-selector-datetime,
|
||||
ha-selector-number {
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
ha-list-item {
|
||||
margin: 0 -24px;
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
.table-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
.stat-list {
|
||||
margin-top: var(--ha-space-2);
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.stat-list ha-spinner {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-statistics-adjust-sum": DialogStatisticsFixUnsupportedUnitMetadata;
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consume } from "../../../../common/decorators/consume";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/chart/statistics-chart";
|
||||
import "../../../../components/date-picker/ha-date-range-picker";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-spinner";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../data/context";
|
||||
import type { Statistics, StatisticType } from "../../../../data/recorder";
|
||||
import {
|
||||
clearStatistics,
|
||||
fetchStatistics,
|
||||
StatisticMeanType,
|
||||
} from "../../../../data/recorder";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { DialogStatisticsClearRangeParams } from "./show-dialog-statistics-clear-range";
|
||||
|
||||
// Longer ranges use hourly statistics to keep the preview fast.
|
||||
const MAX_5MINUTE_PREVIEW_MS = 3 * 24 * 60 * 60 * 1000;
|
||||
|
||||
type PreviewPeriod = "5minute" | "hour";
|
||||
|
||||
// The recorder also clears the period in which the range starts, so the
|
||||
// preview starts at the beginning of that period.
|
||||
const periodStart = (date: Date, period: PreviewPeriod) => {
|
||||
const periodMs = (period === "hour" ? 60 : 5) * 60 * 1000;
|
||||
return new Date(Math.floor(date.getTime() / periodMs) * periodMs);
|
||||
};
|
||||
|
||||
@customElement("dialog-statistics-clear-range")
|
||||
class DialogStatisticsClearRange extends DialogMixin<DialogStatisticsClearRangeParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _start = new Date();
|
||||
|
||||
@state() private _end = new Date();
|
||||
|
||||
@state() private _statistics?: Statistics;
|
||||
|
||||
@state() private _previewStart?: Date;
|
||||
|
||||
@state() private _busy = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
// Incremented for every fetch, so results of outdated fetches are ignored
|
||||
private _fetchId = 0;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._end.setMinutes(
|
||||
this._end.getMinutes() - (this._end.getMinutes() % 5),
|
||||
0,
|
||||
0
|
||||
);
|
||||
this._start = new Date(this._end.getTime() - 24 * 60 * 60 * 1000);
|
||||
this._fetchStatistics();
|
||||
}
|
||||
|
||||
private get _validRange() {
|
||||
return this._start.getTime() < this._end.getTime();
|
||||
}
|
||||
|
||||
private get _statTypes(): StatisticType[] {
|
||||
return this.params!.statistic.mean_type !== StatisticMeanType.NONE
|
||||
? ["mean", "min", "max"]
|
||||
: ["change"];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
const localize = this._i18n.localize;
|
||||
const statisticId = this.params.statistic.statistic_id;
|
||||
const hasData = Boolean(this._statistics?.[statisticId]?.length);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
width="large"
|
||||
.headerTitle=${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.title"
|
||||
)}
|
||||
.preventScrimClose=${this._busy}
|
||||
>
|
||||
<p>
|
||||
${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.info_text",
|
||||
{ name: html`<b>${this.params.name}</b>` }
|
||||
)}
|
||||
</p>
|
||||
<ha-date-range-picker
|
||||
time-picker
|
||||
.startDate=${this._start}
|
||||
.endDate=${this._end}
|
||||
.disabled=${this._busy}
|
||||
@value-changed=${this._rangeChanged}
|
||||
></ha-date-range-picker>
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!this._validRange
|
||||
? html`<ha-alert alert-type="error">
|
||||
${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.invalid_range"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: !this._statistics
|
||||
? html`<div class="preview center">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`
|
||||
: !hasData
|
||||
? html`<div class="preview center">
|
||||
${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.no_statistics_found"
|
||||
)}
|
||||
</div>`
|
||||
: html`<div class="preview">
|
||||
<statistics-chart
|
||||
.statisticsData=${this._statistics}
|
||||
.metadata=${{ [statisticId]: this.params.statistic }}
|
||||
.names=${{ [statisticId]: this.params.name }}
|
||||
.statTypes=${this._statTypes}
|
||||
.chartType=${
|
||||
this._statTypes[0] === "change" ? "bar" : "line"
|
||||
}
|
||||
.startTime=${this._previewStart}
|
||||
.endTime=${this._end}
|
||||
hide-legend
|
||||
.clickForMoreInfo=${false}
|
||||
height="240px"
|
||||
></statistics-chart>
|
||||
</div>`
|
||||
}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
.disabled=${this._busy}
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
variant="danger"
|
||||
.loading=${this._busy}
|
||||
.disabled=${this._busy || !this._validRange || !hasData}
|
||||
@click=${this._delete}
|
||||
>
|
||||
${localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rangeChanged(
|
||||
ev: HASSDomEvent<{ value: { startDate: Date; endDate: Date } }>
|
||||
) {
|
||||
this._start = ev.detail.value.startDate;
|
||||
this._end = ev.detail.value.endDate;
|
||||
this._fetchStatistics();
|
||||
}
|
||||
|
||||
private async _fetchStatistics() {
|
||||
const fetchId = ++this._fetchId;
|
||||
this._statistics = undefined;
|
||||
this._error = undefined;
|
||||
if (!this._validRange) {
|
||||
return;
|
||||
}
|
||||
const statisticId = this.params!.statistic.statistic_id;
|
||||
const fetchPreview = async (period: PreviewPeriod) => {
|
||||
const start = periodStart(this._start, period);
|
||||
const statistics = await fetchStatistics(
|
||||
this._api.callWS,
|
||||
start,
|
||||
this._end,
|
||||
[statisticId],
|
||||
period,
|
||||
undefined,
|
||||
this._statTypes.includes("change") ? ["change"] : ["mean", "min", "max"]
|
||||
);
|
||||
return { start, statistics };
|
||||
};
|
||||
try {
|
||||
const shortRange =
|
||||
this._end.getTime() - this._start.getTime() <= MAX_5MINUTE_PREVIEW_MS;
|
||||
let preview = await fetchPreview(shortRange ? "5minute" : "hour");
|
||||
// Short term statistics are purged before the hourly statistics.
|
||||
if (shortRange && !preview.statistics[statisticId]?.length) {
|
||||
preview = await fetchPreview("hour");
|
||||
}
|
||||
if (fetchId === this._fetchId) {
|
||||
this._previewStart = preview.start;
|
||||
this._statistics = preview.statistics;
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (fetchId === this._fetchId) {
|
||||
this._statistics = {};
|
||||
this._error = err.message || err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _delete() {
|
||||
this._busy = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
await clearStatistics(this._api, [this.params!.statistic.statistic_id], {
|
||||
start: this._start,
|
||||
end: this._end,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._busy = false;
|
||||
this._error = err.message || err;
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this._i18n.localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.deleted"
|
||||
),
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-date-range-picker {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.preview {
|
||||
min-height: 240px;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-statistics-clear-range": DialogStatisticsClearRange;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,30 @@ import type { StatisticsValidationResult } from "../../../../data/recorder";
|
||||
import { showFixStatisticsDialog } from "./show-dialog-statistics-fix";
|
||||
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
|
||||
|
||||
const FIX_ISSUES_ORDER: Record<StatisticsValidationResult["type"], number> = {
|
||||
no_state: 0,
|
||||
entity_no_longer_recorded: 1,
|
||||
entity_not_recorded: 1,
|
||||
state_class_removed: 2,
|
||||
units_changed: 3,
|
||||
mean_type_changed: 4,
|
||||
};
|
||||
|
||||
export const FIXABLE_ISSUES: StatisticsValidationResult["type"][] = [
|
||||
"no_state",
|
||||
"entity_no_longer_recorded",
|
||||
"state_class_removed",
|
||||
"units_changed",
|
||||
"mean_type_changed",
|
||||
];
|
||||
|
||||
export const sortStatisticsIssues = (issues: StatisticsValidationResult[]) =>
|
||||
[...issues].sort(
|
||||
(itemA, itemB) =>
|
||||
(FIX_ISSUES_ORDER[itemA.type] ?? 99) -
|
||||
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
|
||||
);
|
||||
|
||||
export const fixStatisticsIssue = async (
|
||||
element: HTMLElement,
|
||||
issue: StatisticsValidationResult
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
StatisticsMetaData,
|
||||
StatisticsValidationResult,
|
||||
} from "../../../../data/recorder";
|
||||
|
||||
export const loadStatisticDialog = () => import("./dialog-statistic");
|
||||
|
||||
export interface DialogStatisticParams {
|
||||
statistic: StatisticsMetaData;
|
||||
name: string;
|
||||
area?: string;
|
||||
device?: string;
|
||||
issues?: StatisticsValidationResult[];
|
||||
issueFixedCallback?: () => void;
|
||||
}
|
||||
|
||||
export const showStatisticDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DialogStatisticParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-statistic",
|
||||
dialogImport: loadStatisticDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
|
||||
export const loadAdjustSumDialog = () =>
|
||||
import("./dialog-statistics-adjust-sum");
|
||||
|
||||
export interface DialogStatisticsAdjustSumParams {
|
||||
statistic: StatisticsMetaData;
|
||||
}
|
||||
|
||||
export const showStatisticsAdjustSumDialog = (
|
||||
element: HTMLElement,
|
||||
detailParams: DialogStatisticsAdjustSumParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-statistics-adjust-sum",
|
||||
dialogImport: loadAdjustSumDialog,
|
||||
dialogParams: detailParams,
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
|
||||
export const loadClearRangeDialog = () =>
|
||||
import("./dialog-statistics-clear-range");
|
||||
|
||||
export interface DialogStatisticsClearRangeParams {
|
||||
statistic: StatisticsMetaData;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const showStatisticsClearRangeDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DialogStatisticsClearRangeParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-statistics-clear-range",
|
||||
dialogImport: loadClearRangeDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -2,10 +2,8 @@ import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiDeleteClock,
|
||||
mdiFormatListChecks,
|
||||
mdiMenuDown,
|
||||
mdiSlopeUphill,
|
||||
mdiTableCog,
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
@@ -28,11 +26,9 @@ import { consume } from "../../../../common/decorators/consume";
|
||||
import { QueryParamsController } from "../../../../common/controllers/query-params-controller";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import {
|
||||
computeDeviceName,
|
||||
@@ -87,26 +83,7 @@ import type {
|
||||
HomeAssistantRegistries,
|
||||
} from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { fixStatisticsIssue } from "./fix-statistics";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
import { showStatisticsClearRangeDialog } from "./show-dialog-statistics-clear-range";
|
||||
|
||||
const FIX_ISSUES_ORDER: Record<StatisticsValidationResult["type"], number> = {
|
||||
no_state: 0,
|
||||
entity_no_longer_recorded: 1,
|
||||
entity_not_recorded: 1,
|
||||
state_class_removed: 2,
|
||||
units_changed: 3,
|
||||
mean_type_changed: 4,
|
||||
};
|
||||
|
||||
const FIXABLE_ISSUES: StatisticsValidationResult["type"][] = [
|
||||
"no_state",
|
||||
"entity_no_longer_recorded",
|
||||
"state_class_removed",
|
||||
"units_changed",
|
||||
"mean_type_changed",
|
||||
];
|
||||
import { showStatisticDialog } from "./show-dialog-statistic";
|
||||
|
||||
type StatisticData = StatisticsMetaData & {
|
||||
issues?: StatisticsValidationResult[];
|
||||
@@ -318,70 +295,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
localize("ui.panel.config.tools.tabs.statistics.no_issue")
|
||||
}`,
|
||||
},
|
||||
fix: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tools.tabs.statistics.fix_issue.fix"),
|
||||
type: "icon",
|
||||
template: (statistic) =>
|
||||
html`${
|
||||
statistic.issues
|
||||
? html`<ha-button
|
||||
@click=${this._fixIssue}
|
||||
.data=${statistic.issues}
|
||||
appearance="plain"
|
||||
size="s"
|
||||
>
|
||||
${localize(
|
||||
statistic.issues.some((issue) =>
|
||||
FIXABLE_ISSUES.includes(issue.type)
|
||||
)
|
||||
? "ui.panel.config.tools.tabs.statistics.fix_issue.fix"
|
||||
: "ui.panel.config.tools.tabs.statistics.fix_issue.info"
|
||||
)}
|
||||
</ha-button>`
|
||||
: "—"
|
||||
}`,
|
||||
minWidth: "113px",
|
||||
maxWidth: "113px",
|
||||
showNarrow: true,
|
||||
},
|
||||
clear_range: {
|
||||
title: "",
|
||||
label: localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.title"
|
||||
),
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
template: (statistic) => html`
|
||||
<ha-icon-button
|
||||
.label=${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.clear_range.title"
|
||||
)}
|
||||
.path=${mdiDeleteClock}
|
||||
.statistic=${statistic}
|
||||
@click=${this._showStatisticsClearRangeDialog}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tools.tabs.statistics.adjust_sum"),
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
template: (statistic) =>
|
||||
statistic.has_sum
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${localize(
|
||||
"ui.panel.config.tools.tabs.statistics.adjust_sum"
|
||||
)}
|
||||
.path=${mdiSlopeUphill}
|
||||
.statistic=${statistic}
|
||||
@click=${this._showStatisticsAdjustSumDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -825,35 +738,28 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.selectAll((statistic) => statistic.issues);
|
||||
}
|
||||
|
||||
private _showStatisticsAdjustSumDialog(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { statistic: StatisticData }>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: ev.currentTarget.statistic,
|
||||
});
|
||||
}
|
||||
|
||||
private _showStatisticsClearRangeDialog(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { statistic: DisplayedStatisticData }
|
||||
>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const statistic = ev.currentTarget.statistic;
|
||||
showStatisticsClearRangeDialog(this, {
|
||||
private _rowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const statistic = this._displayData(
|
||||
this._data,
|
||||
this._i18n.localize,
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this._registries.areas,
|
||||
this._formatEntityName
|
||||
).find((stat) => stat.statistic_id === ev.detail.id);
|
||||
if (!statistic) {
|
||||
return;
|
||||
}
|
||||
showStatisticDialog(this, {
|
||||
statistic,
|
||||
name: statistic.displayName,
|
||||
area: statistic.area,
|
||||
device: statistic.device,
|
||||
issues: statistic.issues,
|
||||
issueFixedCallback: () => this._validateStatistics(),
|
||||
});
|
||||
}
|
||||
|
||||
private _rowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const id = ev.detail.id;
|
||||
if (id in this._states) {
|
||||
fireEvent(this, "hass-more-info", { entityId: id });
|
||||
}
|
||||
}
|
||||
|
||||
private async _validateStatistics() {
|
||||
try {
|
||||
const [statisticIds, issues] = await Promise.all([
|
||||
@@ -918,21 +824,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _fixIssue = async (
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { data: StatisticsValidationResult[] }
|
||||
>
|
||||
) => {
|
||||
const issues = ev.currentTarget.data.sort(
|
||||
(itemA, itemB) =>
|
||||
(FIX_ISSUES_ORDER[itemA.type] ?? 99) -
|
||||
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
|
||||
);
|
||||
const issue = issues[0];
|
||||
await fixStatisticsIssue(this, issue);
|
||||
this._validateStatistics();
|
||||
};
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
f: () => this._searchInput.focus(),
|
||||
|
||||
+24
-19
@@ -4502,27 +4502,32 @@
|
||||
"info_text_1": "The mean type of ''{name}'' ({statistic_id}) changed from ''{metadata_mean_type}'' to ''{state_mean_type}''.",
|
||||
"info_text_2": "Statistics cannot be generated until the old statistics are deleted, or the mean type matches the old statistics data again.",
|
||||
"info_text_3": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
|
||||
},
|
||||
"adjust_sum": {
|
||||
"title": "Adjust a statistic",
|
||||
"no_statistics_found": "No statistics found for this period.",
|
||||
"info_text_1": "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.",
|
||||
"pick_a_time": "Pick a time",
|
||||
"statistic": "Statistic:",
|
||||
"start": "Start",
|
||||
"end": "End",
|
||||
"new_value": "New value",
|
||||
"adjust": "Adjust",
|
||||
"outliers": "Outliers",
|
||||
"sum_adjusted": "Statistic sum adjusted",
|
||||
"error_sum_adjusted": "Error adjusting sum: {message}",
|
||||
"error_loading_statistics": "Error loading statistics: {message}"
|
||||
}
|
||||
},
|
||||
"adjust_sum": "Adjust sum",
|
||||
"clear_range": {
|
||||
"title": "Delete statistics in a time range",
|
||||
"info_text": "Delete the statistics of {name} between the start and end time. Use this to remove bad measurements from your graphs. Deleted statistics cannot be restored.",
|
||||
"statistic": {
|
||||
"more_info": "[%key:ui::panel::config::tools::tabs::states::more_info%]",
|
||||
"all_data": "All data",
|
||||
"show_all_data": "Show all data",
|
||||
"data_range": "Recorded data",
|
||||
"correct_mode": "Correction mode",
|
||||
"no_selection": "No period selected",
|
||||
"outliers": "Outliers",
|
||||
"no_outliers": "[%key:ui::common::none%]",
|
||||
"correct": "Correct data",
|
||||
"correct_sum_hint": "Select the bar with the wrong value.",
|
||||
"correct_mean_hint": "Select the period with wrong values, or drag across the graph to select several periods.",
|
||||
"current_value": "Current value",
|
||||
"period": "From {start} to {end}",
|
||||
"mean": "[%key:ui::panel::lovelace::editor::card::statistic::stat_type_labels::mean%]",
|
||||
"min": "[%key:ui::panel::lovelace::editor::card::statistic::stat_type_labels::min%]",
|
||||
"max": "[%key:ui::panel::lovelace::editor::card::statistic::stat_type_labels::max%]",
|
||||
"new_value": "New value",
|
||||
"adjust": "Adjust",
|
||||
"adjusted": "Statistic adjusted",
|
||||
"delete": "Delete period",
|
||||
"delete_confirm_title": "Delete these statistics?",
|
||||
"delete_confirm_text": "Deleted statistics cannot be restored.",
|
||||
"delete_confirm_text_5minute": "The hourly statistics that overlap this period are deleted too. Deleted statistics cannot be restored.",
|
||||
"invalid_range": "The end time must be after the start time.",
|
||||
"no_statistics_found": "No statistics found for this period.",
|
||||
"deleted": "Statistics deleted"
|
||||
|
||||
Reference in new issue
Block a user