ha-frontend/src/panels/lovelace/cards/energy/hui-energy-devices-detail-g...

359 lines
9.1 KiB
TypeScript

import {
ChartData,
ChartDataset,
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import { endOfToday, startOfToday } from "date-fns/esm";
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { getColorByIndex } from "../../../../common/color/colors";
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
DeviceConsumptionEnergyPreference,
EnergyData,
getEnergyDataCollection,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
getStatisticLabel,
Statistics,
StatisticsMetaData,
} from "../../../../data/recorder";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDevicesDetailGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options";
const UNIT = "kWh";
@customElement("hui-energy-devices-detail-graph-card")
export class HuiEnergyDevicesDetailGraphCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyDevicesDetailGraphCardConfig;
@state() private _chartData: ChartData = { datasets: [] };
@state() private _chartDatasetExtra: ChartDatasetExtra[] = [];
@state() private _data?: EnergyData;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
@state() private _hiddenStats = new Set<string>();
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
key: this._config?.collection_key,
}).subscribe((data) => {
this._data = data;
this._processStatistics();
}),
];
}
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: EnergyDevicesDetailGraphCardConfig): void {
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
hasConfigChanged(this, changedProps) ||
changedProps.size > 1 ||
!changedProps.has("hass")
);
}
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("_hiddenStats") && this._data) {
this._processStatistics();
}
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
})}"
>
<ha-chart-base
externalHidden
.hass=${this.hass}
.data=${this._chartData}
.extraData=${this._chartDatasetExtra}
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale,
this.hass.config,
UNIT,
this._compareStart,
this._compareEnd
)}
chart-type="bar"
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
</div>
</ha-card>
`;
}
private _datasetHidden(ev) {
ev.stopPropagation();
this._hiddenStats.add(
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev) {
ev.stopPropagation();
this._hiddenStats.delete(
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
);
this.requestUpdate("_hiddenStats");
}
private _createOptions = memoizeOne(
(
start: Date,
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const commonOptions = getCommonOptions(
start,
end,
locale,
config,
unit,
compareStart,
compareEnd
);
const options: ChartOptions = {
...commonOptions,
interaction: {
mode: "nearest",
},
plugins: {
...commonOptions.plugins!,
legend: {
display: true,
labels: {
usePointStyle: true,
},
},
},
};
return options;
}
);
private _processStatistics() {
const energyData = this._data!;
const data = energyData.stats;
const compareData = energyData.statsCompare;
const growthValues = {};
energyData.prefs.device_consumption.forEach((device) => {
const value =
device.stat_consumption in data
? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
: 0;
growthValues[device.stat_consumption] = value;
});
const sorted_devices = energyData.prefs.device_consumption.map(
(device) => device.stat_consumption
);
sorted_devices.sort((a, b) => growthValues[b] - growthValues[a]);
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
const datasetExtras: ChartDatasetExtra[] = [];
datasets.push(
...this._processDataSet(
data,
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices
)
);
const items = datasets.length;
datasetExtras.push(...Array<ChartDatasetExtra>(items).fill({}));
if (compareData) {
// Add empty dataset to align the bars
datasets.push({
order: 0,
data: [],
});
datasetExtras.push({
show_legend: false,
});
datasets.push({
order: 999,
data: [],
xAxisID: "xAxisCompare",
});
datasetExtras.push({
show_legend: false,
});
datasets.push(
...this._processDataSet(
compareData,
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices,
true
)
);
datasetExtras.push(
...Array<ChartDatasetExtra>(items).fill({ show_legend: false })
);
}
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._compareStart = energyData.startCompare;
this._compareEnd = energyData.endCompare;
this._chartData = {
datasets,
};
this._chartDatasetExtra = datasetExtras;
}
private _processDataSet(
statistics: Statistics,
statisticsMetaData: Record<string, StatisticsMetaData>,
devices: DeviceConsumptionEnergyPreference[],
sorted_devices: string[],
compare = false
) {
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
devices.forEach((source, idx) => {
const color = getColorByIndex(idx);
let prevStart: number | null = null;
const consumptionData: ScatterDataPoint[] = [];
// Process gas consumption data.
if (source.stat_consumption in statistics) {
const stats = statistics[source.stat_consumption];
let end;
for (const point of stats) {
if (point.change === null || point.change === undefined) {
continue;
}
if (prevStart === point.start) {
continue;
}
const date = new Date(point.start);
consumptionData.push({
x: date.getTime(),
y: point.change,
});
prevStart = point.start;
end = point.end;
}
if (consumptionData.length === 1) {
consumptionData.push({
x: end,
y: 0,
});
}
}
data.push({
label: getStatisticLabel(
this.hass,
source.stat_consumption,
statisticsMetaData[source.stat_consumption]
),
hidden: this._hiddenStats.has(source.stat_consumption),
borderColor: compare ? color + "7F" : color,
backgroundColor: compare ? color + "32" : color + "7F",
data: consumptionData,
order: 1 + sorted_devices.indexOf(source.stat_consumption),
stack: "devices",
pointStyle: compare ? false : "circle",
xAxisID: compare ? "xAxisCompare" : undefined,
});
});
return data;
}
static get styles(): CSSResultGroup {
return css`
.card-header {
padding-bottom: 0;
}
.content {
padding: 16px;
}
.has-header {
padding-top: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-devices-detail-graph-card": HuiEnergyDevicesDetailGraphCard;
}
}