mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Merge pull request #9659 from home-assistant/dev
This commit is contained in:
commit
378e6d28bc
42
package.json
42
package.json
@ -43,24 +43,24 @@
|
||||
"@fullcalendar/interaction": "5.1.0",
|
||||
"@fullcalendar/list": "5.1.0",
|
||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
|
||||
"@material/chips": "=12.0.0-canary.1a8d06483.0",
|
||||
"@material/data-table": "=12.0.0-canary.1a8d06483.0",
|
||||
"@material/mwc-button": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-checkbox": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-circular-progress": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-dialog": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-fab": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-formfield": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-icon-button": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-linear-progress": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-list": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-menu": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-radio": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-ripple": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-switch": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-tab": "0.22.0-canary.cc04657a.0",
|
||||
"@material/mwc-tab-bar": "0.22.0-canary.cc04657a.0",
|
||||
"@material/top-app-bar": "=12.0.0-canary.1a8d06483.0",
|
||||
"@material/chips": "12.0.0-canary.22d29cbb4.0",
|
||||
"@material/data-table": "12.0.0-canary.22d29cbb4.0",
|
||||
"@material/mwc-button": "0.22.1",
|
||||
"@material/mwc-checkbox": "0.22.1",
|
||||
"@material/mwc-circular-progress": "0.22.1",
|
||||
"@material/mwc-dialog": "0.22.1",
|
||||
"@material/mwc-fab": "0.22.1",
|
||||
"@material/mwc-formfield": "0.22.1",
|
||||
"@material/mwc-icon-button": "0.22.1",
|
||||
"@material/mwc-linear-progress": "0.22.1",
|
||||
"@material/mwc-list": "0.22.1",
|
||||
"@material/mwc-menu": "0.22.1",
|
||||
"@material/mwc-radio": "0.22.1",
|
||||
"@material/mwc-ripple": "0.22.1",
|
||||
"@material/mwc-switch": "0.22.1",
|
||||
"@material/mwc-tab": "0.22.1",
|
||||
"@material/mwc-tab-bar": "0.22.1",
|
||||
"@material/top-app-bar": "12.0.0-canary.22d29cbb4.0",
|
||||
"@mdi/js": "5.9.55",
|
||||
"@mdi/svg": "5.9.55",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
@ -89,8 +89,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.5.2",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||
"@vaadin/vaadin-combo-box": "^20.0.1",
|
||||
"@vaadin/vaadin-date-picker": "^20.0.1",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
@ -107,7 +107,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.7",
|
||||
"home-assistant-js-websocket": "^5.10.0",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"idb-keyval": "^5.0.5",
|
||||
"intl-messageformat": "^9.6.16",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210726.0",
|
||||
version="20210730.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -63,6 +63,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
temperature: "hass:thermometer",
|
||||
monetary: "mdi:cash",
|
||||
pressure: "hass:gauge",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
|
@ -21,6 +21,16 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
style: "currency",
|
||||
currency: stateObj.attributes.unit_of_measurement,
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
return `${formatNumber(compareState, locale)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
|
@ -1,4 +1,22 @@
|
||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
import { round } from "../number/round";
|
||||
|
||||
export const numberFormatToLocale = (
|
||||
localeOptions: FrontendLocaleData
|
||||
): string | string[] | undefined => {
|
||||
switch (localeOptions.number_format) {
|
||||
case NumberFormat.comma_decimal:
|
||||
return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
|
||||
case NumberFormat.decimal_comma:
|
||||
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
|
||||
case NumberFormat.space_comma:
|
||||
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
|
||||
case NumberFormat.system:
|
||||
return undefined;
|
||||
default:
|
||||
return localeOptions.language;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
@ -9,27 +27,12 @@ import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
locale?: FrontendLocaleData,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
let format: string | string[] | undefined;
|
||||
|
||||
switch (locale?.number_format) {
|
||||
case NumberFormat.comma_decimal:
|
||||
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
|
||||
break;
|
||||
case NumberFormat.decimal_comma:
|
||||
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
|
||||
break;
|
||||
case NumberFormat.space_comma:
|
||||
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
|
||||
break;
|
||||
case NumberFormat.system:
|
||||
format = undefined;
|
||||
break;
|
||||
default:
|
||||
format = locale?.language;
|
||||
}
|
||||
const locale = localeOptions
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
@ -39,13 +42,13 @@ export const formatNumber = (
|
||||
};
|
||||
|
||||
if (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num)) &&
|
||||
Intl &&
|
||||
locale?.number_format !== NumberFormat.none
|
||||
Intl
|
||||
) {
|
||||
try {
|
||||
return new Intl.NumberFormat(
|
||||
format,
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
} catch (error) {
|
||||
@ -58,7 +61,12 @@ export const formatNumber = (
|
||||
).format(Number(num));
|
||||
}
|
||||
}
|
||||
return num.toString();
|
||||
if (typeof num === "string") {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === "currency" ? ` ${options.currency}` : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -5,7 +5,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import "../ha-circular-progress";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
class HaProgressButton extends LitElement {
|
||||
export class HaProgressButton extends LitElement {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
|
@ -55,7 +55,7 @@ export default class HaChartBase extends LitElement {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("type")) {
|
||||
if (changedProps.has("chartType")) {
|
||||
this.chart.config.type = this.chartType;
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
@ -136,12 +136,9 @@ export default class HaChartBase extends LitElement {
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
${this._tooltip.footer
|
||||
? // footer has white-space: pre;
|
||||
// prettier-ignore
|
||||
html`<div class="footer">${Array.isArray(this._tooltip.footer)
|
||||
? this._tooltip.footer.join("\n")
|
||||
: this._tooltip.footer}
|
||||
${this._tooltip.footer.length
|
||||
? html`<div class="footer">
|
||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>`
|
||||
@ -155,7 +152,17 @@ export default class HaChartBase extends LitElement {
|
||||
.querySelector("canvas")!
|
||||
.getContext("2d")!;
|
||||
|
||||
this.chart = new (await import("../../resources/chartjs")).Chart(ctx, {
|
||||
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
ChartConstructor.defaults.borderColor =
|
||||
computedStyles.getPropertyValue("--divider-color");
|
||||
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
);
|
||||
|
||||
this.chart = new ChartConstructor(ctx, {
|
||||
type: this.chartType,
|
||||
data: this.data,
|
||||
options: this._createOptions(),
|
||||
@ -275,7 +282,7 @@ export default class HaChartBase extends LitElement {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
margin-right: 6px;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
@ -283,9 +290,10 @@ export default class HaChartBase extends LitElement {
|
||||
.chartTooltip .bullet {
|
||||
align-self: baseline;
|
||||
}
|
||||
:host([rtl]) .chartLegend .bullet,
|
||||
:host([rtl]) .chartTooltip .bullet {
|
||||
margin-right: inherit;
|
||||
margin-left: 4px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.chartTooltip {
|
||||
padding: 8px;
|
||||
@ -317,6 +325,7 @@ export default class HaChartBase extends LitElement {
|
||||
white-space: pre-line;
|
||||
align-items: center;
|
||||
line-height: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.chartTooltip .title {
|
||||
text-align: center;
|
||||
@ -324,7 +333,6 @@ export default class HaChartBase extends LitElement {
|
||||
}
|
||||
.chartTooltip .footer {
|
||||
font-weight: 500;
|
||||
white-space: pre;
|
||||
}
|
||||
.chartTooltip .beforeBody {
|
||||
text-align: center;
|
||||
|
@ -2,6 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { numberFormatToLocale } from "../../common/string/format_number";
|
||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
@ -109,6 +110,8 @@ class StateHistoryChartLine extends LitElement {
|
||||
hitRadius: 5,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
|
@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { numberFormatToLocale } from "../../common/string/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@ -186,6 +187,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
propagate: true,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
|
@ -16,6 +16,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { numberFormatToLocale } from "../../common/string/format_number";
|
||||
import {
|
||||
Statistics,
|
||||
statisticsHaveType,
|
||||
@ -37,8 +38,8 @@ class StatisticsChart extends LitElement {
|
||||
@property({ type: Array }) public statTypes: Array<StatisticType> = [
|
||||
"sum",
|
||||
"min",
|
||||
"max",
|
||||
"mean",
|
||||
"max",
|
||||
];
|
||||
|
||||
@property() public chartType: ChartType = "line";
|
||||
@ -57,7 +58,7 @@ class StatisticsChart extends LitElement {
|
||||
if (!this.hasUpdated) {
|
||||
this._createOptions();
|
||||
}
|
||||
if (changedProps.has("statisticsData")) {
|
||||
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@ -119,7 +120,7 @@ class StatisticsChart extends LitElement {
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetimeseconds",
|
||||
tooltipFormat: "datetime",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
@ -157,10 +158,15 @@ class StatisticsChart extends LitElement {
|
||||
hitRadius: 5,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
|
||||
private _generateData() {
|
||||
if (!this.statisticsData) {
|
||||
return;
|
||||
}
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.values(this.statisticsData);
|
||||
const totalDataSets: ChartDataset<"line">[] = [];
|
||||
@ -228,21 +234,21 @@ class StatisticsChart extends LitElement {
|
||||
prevValues = dataValues;
|
||||
};
|
||||
|
||||
const color = getColorByIndex(colorIndex);
|
||||
colorIndex++;
|
||||
|
||||
const addDataSet = (
|
||||
nameY: string,
|
||||
borderColor: string,
|
||||
backgroundColor: string,
|
||||
step = false,
|
||||
fill = false,
|
||||
color?: string
|
||||
fill?: boolean | number | string
|
||||
) => {
|
||||
if (!color) {
|
||||
color = getColorByIndex(colorIndex);
|
||||
colorIndex++;
|
||||
}
|
||||
statDataSets.push({
|
||||
label: nameY,
|
||||
fill: fill ? "origin" : false,
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
fill: fill || false,
|
||||
borderColor,
|
||||
backgroundColor: backgroundColor,
|
||||
stepped: step ? "before" : false,
|
||||
pointRadius: 0,
|
||||
data: [],
|
||||
@ -251,26 +257,60 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
const statTypes: this["statTypes"] = [];
|
||||
|
||||
this.statTypes.forEach((type) => {
|
||||
const sortedTypes = [...this.statTypes].sort((a, _b) => {
|
||||
if (a === "min") {
|
||||
return -1;
|
||||
}
|
||||
if (a === "max") {
|
||||
return +1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const drawBands =
|
||||
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
|
||||
|
||||
sortedTypes.forEach((type) => {
|
||||
if (statisticsHaveType(stats, type)) {
|
||||
statTypes.push(type);
|
||||
addDataSet(
|
||||
`${name} (${this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
)})`,
|
||||
false
|
||||
drawBands && (type === "min" || type === "max")
|
||||
? color + "7F"
|
||||
: color,
|
||||
color + "7F",
|
||||
false,
|
||||
drawBands
|
||||
? type === "min"
|
||||
? "+1"
|
||||
: type === "max"
|
||||
? "-1"
|
||||
: false
|
||||
: false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let prevDate: Date | null = null;
|
||||
// Process chart data.
|
||||
stats.forEach((stat) => {
|
||||
const date = new Date(stat.start);
|
||||
if (prevDate === date) {
|
||||
return;
|
||||
}
|
||||
prevDate = date;
|
||||
const dataValues: Array<number | null> = [];
|
||||
statTypes.forEach((type) => {
|
||||
const val = stat[type];
|
||||
let val: number | null;
|
||||
if (type === "sum") {
|
||||
val = stat.state;
|
||||
} else {
|
||||
val = stat[type];
|
||||
}
|
||||
dataValues.push(val !== null ? Math.round(val * 100) / 100 : null);
|
||||
});
|
||||
const date = new Date(stat.start);
|
||||
pushData(date, dataValues);
|
||||
});
|
||||
|
||||
|
168
src/components/currency-datalist.ts
Normal file
168
src/components/currency-datalist.ts
Normal file
@ -0,0 +1,168 @@
|
||||
export const createCurrencyListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BHD",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BTN",
|
||||
"BWP",
|
||||
"BYR",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CUP",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EGP",
|
||||
"ERN",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GHS",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"IQD",
|
||||
"IRR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JOD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KPW",
|
||||
"KRW",
|
||||
"KWD",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LYD",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MMK",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"OMR",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SDG",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"SSP",
|
||||
"STD",
|
||||
"SYP",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TMT",
|
||||
"TND",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VEF",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZWL",
|
||||
]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerHTML = currency;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
};
|
@ -131,6 +131,9 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
private async _addEntity(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
|
@ -22,46 +22,12 @@ import { compare } from "../../common/string/compare";
|
||||
import { getStatisticIds, StatisticsMetaData } from "../../data/history";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
// vaadin-combo-box-item
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<{
|
||||
id: string;
|
||||
name: string;
|
||||
state?: HassEntity;
|
||||
}> = (item) => html`<style>
|
||||
paper-icon-item {
|
||||
padding: 0;
|
||||
margin: -8px;
|
||||
}
|
||||
#content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding-left: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
:host(:not([selected])) ha-svg-icon {
|
||||
display: none;
|
||||
}
|
||||
:host([selected]) paper-icon-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
|
||||
<paper-item-body two-line="">
|
||||
${item.name}
|
||||
<span secondary>${item.id}</span>
|
||||
</paper-item-body>
|
||||
</paper-icon-item>`;
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -99,6 +65,53 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private _init = false;
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<{
|
||||
id: string;
|
||||
name: string;
|
||||
state?: HassEntity;
|
||||
}> = (item) => html`<style>
|
||||
paper-icon-item {
|
||||
padding: 0;
|
||||
margin: -8px;
|
||||
}
|
||||
#content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding-left: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
:host(:not([selected])) ha-svg-icon {
|
||||
display: none;
|
||||
}
|
||||
:host([selected]) paper-icon-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
|
||||
<paper-item-body two-line="">
|
||||
${item.name}
|
||||
<span secondary
|
||||
>${item.id === "" || item.id === "__missing"
|
||||
? html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="${documentationUrl(this.hass, "/more-info/statistics/")}"
|
||||
>${this.hass.localize(
|
||||
"ui.components.statistic-picker.learn_more"
|
||||
)}</a
|
||||
>`
|
||||
: item.id}</span
|
||||
>
|
||||
</paper-item-body>
|
||||
</paper-icon-item>`;
|
||||
|
||||
private _getStatistics = memoizeOne(
|
||||
(
|
||||
statisticIds: StatisticsMetaData[],
|
||||
@ -110,7 +123,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
{
|
||||
id: "",
|
||||
name: this.hass.localize(
|
||||
"ui.components.statistics-picker.no_statistics"
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -142,10 +155,27 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
});
|
||||
|
||||
if (output.length === 1) {
|
||||
return output;
|
||||
if (!output.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
name: this.hass.localize("ui.components.statistic-picker.no_match"),
|
||||
},
|
||||
];
|
||||
}
|
||||
return output.sort((a, b) => compare(a.name || "", b.name || ""));
|
||||
|
||||
if (output.length > 1) {
|
||||
output.sort((a, b) => compare(a.name || "", b.name || ""));
|
||||
}
|
||||
|
||||
output.push({
|
||||
id: "__missing",
|
||||
name: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
||||
@ -195,7 +225,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
? this.hass.localize("ui.components.statistic-picker.statistic")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.renderer=${this._rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
@ -216,7 +246,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private _statisticChanged(ev: PolymerChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
let newValue = ev.detail.value;
|
||||
if (newValue === "__missing") {
|
||||
newValue = "";
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
|
@ -90,6 +90,9 @@ class HaStatisticsPicker extends LitElement {
|
||||
private async _addStatistic(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
|
@ -13,6 +13,11 @@ const getAngle = (value: number, min: number, max: number) => {
|
||||
return (percentage * 180) / 100;
|
||||
};
|
||||
|
||||
export interface LevelDefinition {
|
||||
level: number;
|
||||
stroke: string;
|
||||
}
|
||||
|
||||
@customElement("ha-gauge")
|
||||
export class Gauge extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
@ -21,8 +26,14 @@ export class Gauge extends LitElement {
|
||||
|
||||
@property({ type: Number }) public value = 0;
|
||||
|
||||
@property({ type: String }) public valueText?: string;
|
||||
|
||||
@property() public locale!: FrontendLocaleData;
|
||||
|
||||
@property({ type: Boolean }) public needle?: boolean;
|
||||
|
||||
@property() public levels?: LevelDefinition[];
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@state() private _angle = 0;
|
||||
@ -51,22 +62,61 @@ export class Gauge extends LitElement {
|
||||
protected render() {
|
||||
return svg`
|
||||
<svg viewBox="0 0 100 50" class="gauge">
|
||||
<path
|
||||
${
|
||||
!this.needle || !this.levels
|
||||
? svg`<path
|
||||
class="dial"
|
||||
d="M 10 50 A 40 40 0 0 1 90 50"
|
||||
></path>
|
||||
<path
|
||||
class="value"
|
||||
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
)}
|
||||
>
|
||||
></path>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
this.levels
|
||||
? this.levels
|
||||
.sort((a, b) => a.level - b.level)
|
||||
.map((level) => {
|
||||
const angle = getAngle(level.level, this.min, this.max);
|
||||
return svg`<path
|
||||
stroke="${level.stroke}"
|
||||
class="level"
|
||||
d="M
|
||||
${50 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||
${50 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||
A 40 40 0 0 1 90 50
|
||||
"
|
||||
></path>`;
|
||||
})
|
||||
: ""
|
||||
}
|
||||
${
|
||||
this.needle
|
||||
? svg`<path
|
||||
class="needle"
|
||||
d="M 25 47.5 L 2.5 50 L 25 52.5 z"
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
)}
|
||||
>
|
||||
`
|
||||
: svg`<path
|
||||
class="value"
|
||||
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
)}
|
||||
>`
|
||||
}
|
||||
${
|
||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||
isSafari
|
||||
@ -83,7 +133,9 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${formatNumber(this.value, this.locale)} ${this.label}
|
||||
${this.valueText || formatNumber(this.value, this.locale)} ${
|
||||
this.label
|
||||
}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
@ -117,6 +169,15 @@ export class Gauge extends LitElement {
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.needle {
|
||||
fill: var(--primary-text-color);
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 15;
|
||||
}
|
||||
.gauge {
|
||||
display: block;
|
||||
}
|
||||
|
@ -53,9 +53,10 @@ const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
map: 1,
|
||||
logbook: 2,
|
||||
history: 3,
|
||||
energy: 1,
|
||||
map: 2,
|
||||
logbook: 3,
|
||||
history: 4,
|
||||
"developer-tools": 9,
|
||||
hassio: 10,
|
||||
config: 11,
|
||||
|
@ -10,6 +10,7 @@ export interface ConfigUpdateValues {
|
||||
time_zone: string;
|
||||
external_url?: string | null;
|
||||
internal_url?: string | null;
|
||||
currency?: string | null;
|
||||
}
|
||||
|
||||
export interface CheckConfigResult {
|
||||
|
8
src/data/currency.ts
Normal file
8
src/data/currency.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const SYMBOL_TO_ISO = {
|
||||
$: "USD",
|
||||
"€": "EUR",
|
||||
"¥": "JPY",
|
||||
"£": "GBP",
|
||||
"₽": "RUB",
|
||||
"₹": "INR",
|
||||
};
|
@ -85,7 +85,6 @@ type EnergySource =
|
||||
| GridSourceTypeEnergyPreference;
|
||||
|
||||
export interface EnergyPreferences {
|
||||
currency: string;
|
||||
energy_sources: EnergySource[];
|
||||
device_consumption: DeviceConsumptionEnergyPreference[];
|
||||
}
|
||||
|
@ -302,12 +302,9 @@ export const fetchStatistics = (
|
||||
export const calculateStatisticSumGrowth = (
|
||||
values: StatisticValue[]
|
||||
): number | null => {
|
||||
if (values.length === 0) {
|
||||
if (!values || values.length < 2) {
|
||||
return null;
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return values[0].sum;
|
||||
}
|
||||
const endSum = values[values.length - 1].sum;
|
||||
if (endSum === null) {
|
||||
return null;
|
||||
@ -323,19 +320,22 @@ export const calculateStatisticsSumGrowth = (
|
||||
data: Statistics,
|
||||
stats: string[]
|
||||
): number | null => {
|
||||
let totalGrowth = 0;
|
||||
let totalGrowth: number | null = null;
|
||||
|
||||
for (const stat of stats) {
|
||||
if (!(stat in data)) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
const statGrowth = calculateStatisticSumGrowth(data[stat]);
|
||||
|
||||
if (statGrowth === null) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
if (totalGrowth === null) {
|
||||
totalGrowth = statGrowth;
|
||||
} else {
|
||||
totalGrowth += statGrowth;
|
||||
}
|
||||
|
||||
totalGrowth += statGrowth;
|
||||
}
|
||||
|
||||
return totalGrowth;
|
||||
@ -345,3 +345,128 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
/**
|
||||
* Get the earliest start from a list of statistics.
|
||||
*/
|
||||
const getMinStatisticStart = (stats: StatisticValue[][]): string | null => {
|
||||
let earliestString: string | null = null;
|
||||
let earliestTime: Date | null = null;
|
||||
|
||||
for (const stat of stats) {
|
||||
if (stat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const curTime = new Date(stat[0].start);
|
||||
|
||||
if (earliestString === null) {
|
||||
earliestString = stat[0].start;
|
||||
earliestTime = curTime;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (curTime < earliestTime!) {
|
||||
earliestString = stat[0].start;
|
||||
earliestTime = curTime;
|
||||
}
|
||||
}
|
||||
|
||||
return earliestString;
|
||||
};
|
||||
|
||||
// Merge multiple sum statistics into one
|
||||
const mergeSumStatistics = (stats: StatisticValue[][]) => {
|
||||
const result: { start: string; sum: number }[] = [];
|
||||
|
||||
const statsCopy: StatisticValue[][] = stats.map((stat) => [...stat]);
|
||||
|
||||
while (statsCopy.some((stat) => stat.length > 0)) {
|
||||
const earliestStart = getMinStatisticStart(statsCopy)!;
|
||||
|
||||
let sum = 0;
|
||||
|
||||
for (const stat of statsCopy) {
|
||||
if (stat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (stat[0].start !== earliestStart) {
|
||||
continue;
|
||||
}
|
||||
const statVal = stat.shift()!;
|
||||
if (!statVal.sum) {
|
||||
continue;
|
||||
}
|
||||
sum += statVal.sum;
|
||||
}
|
||||
|
||||
result.push({
|
||||
start: earliestStart,
|
||||
sum,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the growth of a statistic over the given period while applying a
|
||||
* per-period percentage.
|
||||
*/
|
||||
export const calculateStatisticsSumGrowthWithPercentage = (
|
||||
percentageStat: StatisticValue[],
|
||||
sumStats: StatisticValue[][]
|
||||
): number | null => {
|
||||
let sum: number | null = null;
|
||||
|
||||
if (sumStats.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sumStatsToProcess = mergeSumStatistics(sumStats);
|
||||
const percentageStatToProcess = [...percentageStat];
|
||||
|
||||
let lastSum: number | null = null;
|
||||
|
||||
// pre-populate lastSum with last sum statistic _before_ the first percentage statistic
|
||||
for (const stat of sumStatsToProcess) {
|
||||
if (new Date(stat.start) >= new Date(percentageStat[0].start)) {
|
||||
break;
|
||||
}
|
||||
lastSum = stat.sum;
|
||||
}
|
||||
|
||||
while (percentageStatToProcess.length > 0) {
|
||||
if (!sumStatsToProcess.length) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
// If they are not equal, pop the value that is earlier in time
|
||||
if (sumStatsToProcess[0].start !== percentageStatToProcess[0].start) {
|
||||
if (
|
||||
new Date(sumStatsToProcess[0].start) <
|
||||
new Date(percentageStatToProcess[0].start)
|
||||
) {
|
||||
sumStatsToProcess.shift();
|
||||
} else {
|
||||
percentageStatToProcess.shift();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const sumStatValue = sumStatsToProcess.shift()!;
|
||||
const percentageStatValue = percentageStatToProcess.shift()!;
|
||||
|
||||
if (lastSum !== null) {
|
||||
const sumGrowth = sumStatValue.sum! - lastSum;
|
||||
if (sum === null) {
|
||||
sum = sumGrowth * (percentageStatValue.mean! / 100);
|
||||
} else {
|
||||
sum += sumGrowth * (percentageStatValue.mean! / 100);
|
||||
}
|
||||
}
|
||||
|
||||
lastSum = sumStatValue.sum;
|
||||
}
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
@ -83,6 +83,12 @@ export interface ZWaveJSHealNetworkStatusMessage {
|
||||
heal_node_status: { [key: number]: string };
|
||||
}
|
||||
|
||||
export interface ZWaveJSRemovedNode {
|
||||
node_id: number;
|
||||
manufacturer: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
Unknown,
|
||||
Asleep,
|
||||
@ -178,6 +184,32 @@ export const reinterviewNode = (
|
||||
}
|
||||
);
|
||||
|
||||
export const healNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
): Promise<boolean> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/heal_node",
|
||||
entry_id: entry_id,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const removeFailedNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
callbackFunction: (message: any) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/remove_failed_node",
|
||||
entry_id: entry_id,
|
||||
node_id: node_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const healNetwork = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
|
@ -22,4 +22,5 @@ export const demoConfig: HassConfig = {
|
||||
state: STATE_RUNNING,
|
||||
internal_url: "http://homeassistant.local:8123",
|
||||
external_url: null,
|
||||
currency: "USD",
|
||||
};
|
||||
|
@ -132,6 +132,7 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
|
||||
.value=${before?.startsWith("input_datetime.") ? before : ""}
|
||||
@value-changed=${this._valueChanged}
|
||||
.hass=${this.hass}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`
|
||||
: html`<paper-input
|
||||
.label=${this.hass.localize(
|
||||
|
@ -7,11 +7,13 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { createCurrencyListEl } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@ -23,6 +25,8 @@ class ConfigCoreForm extends LitElement {
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _elevation?: string;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
@ -143,6 +147,33 @@ class ConfigCoreForm extends LitElement {
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}<br />
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
list="currencies"
|
||||
.disabled=${disabled}
|
||||
.value=${this._currencyValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
@ -157,10 +188,16 @@ class ConfigCoreForm extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
const input = this.shadowRoot!.querySelector(
|
||||
|
||||
const tzInput = this.shadowRoot!.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as PaperInputElement;
|
||||
input.inputElement.appendChild(createTimezoneListEl());
|
||||
tzInput.inputElement.appendChild(createTimezoneListEl());
|
||||
|
||||
const cInput = this.shadowRoot!.querySelector(
|
||||
"[name=currency]"
|
||||
) as PaperInputElement;
|
||||
cInput.inputElement.appendChild(createCurrencyListEl());
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
@ -178,6 +215,12 @@ class ConfigCoreForm extends LitElement {
|
||||
]
|
||||
);
|
||||
|
||||
private get _currencyValue() {
|
||||
return this._currency !== undefined
|
||||
? this._currency
|
||||
: this.hass.config.currency;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined
|
||||
? this._elevation
|
||||
@ -200,7 +243,15 @@ class ConfigCoreForm extends LitElement {
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
@ -223,12 +274,13 @@ class ConfigCoreForm extends LitElement {
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: location[0],
|
||||
longitude: location[1],
|
||||
currency: this._currencyValue,
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue,
|
||||
});
|
||||
} catch (err) {
|
||||
alert("FAIL");
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
@ -258,6 +310,10 @@ class ConfigCoreForm extends LitElement {
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
|
||||
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
|
||||
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
|
||||
|
||||
@customElement("ha-device-actions-zwave_js")
|
||||
export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
@ -56,6 +58,14 @@ export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
"ui.panel.config.zwave_js.device_info.reinterview_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._healNodeClicked}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.device_info.heal_node")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._removeFailedNode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.remove_failed"
|
||||
)}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -69,6 +79,27 @@ export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _healNodeClicked() {
|
||||
if (!this._nodeId || !this._entryId) {
|
||||
return;
|
||||
}
|
||||
showZWaveJSHealNodeDialog(this, {
|
||||
entry_id: this._entryId,
|
||||
node_id: this._nodeId,
|
||||
device: this.device,
|
||||
});
|
||||
}
|
||||
|
||||
private async _removeFailedNode() {
|
||||
if (!this._nodeId || !this._entryId) {
|
||||
return;
|
||||
}
|
||||
showZWaveJSRemoveFailedNodeDialog(this, {
|
||||
entry_id: this._entryId,
|
||||
node_id: this._nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { showEnergySettingsDeviceDialog } from "../dialogs/show-dialogs-energy";
|
||||
import { energyCardStyles } from "./styles";
|
||||
|
||||
@ -33,12 +34,29 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
return html`
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon>Monitor individual
|
||||
devices
|
||||
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.title"
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<div class="card-content">
|
||||
<p>Monitor individual devices.</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.sub"
|
||||
)}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/energy/individual-devices/"
|
||||
)}"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.learn_more"
|
||||
)}</a
|
||||
>
|
||||
</p>
|
||||
<h3>Devices</h3>
|
||||
${this.preferences.device_consumption.map((device) => {
|
||||
const entityState = this.hass.states[device.stat_consumption];
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
energySourcesByType,
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
GridSourceTypeEnergyPreference,
|
||||
saveEnergyPreferences,
|
||||
} from "../../../../data/energy";
|
||||
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
@ -33,6 +34,7 @@ import {
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import {
|
||||
showEnergySettingsGridFlowFromDialog,
|
||||
showEnergySettingsGridFlowToDialog,
|
||||
@ -62,12 +64,25 @@ export class EnergyGridSettings extends LitElement {
|
||||
return html`
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon
|
||||
>${this.hass.localize("ui.panel.config.energy.grid.title")}
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.energy.grid.title")}
|
||||
</h1>
|
||||
|
||||
<div class="card-content">
|
||||
<p>${this.hass.localize("ui.panel.config.energy.grid.sub")}</p>
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.energy.grid.sub")}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/energy/electricity-grid/"
|
||||
)}"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.learn_more"
|
||||
)}</a
|
||||
>
|
||||
</p>
|
||||
<h3>Grid consumption</h3>
|
||||
${gridSource.flow_from.map((flow) => {
|
||||
const entityState = this.hass.states[flow.stat_energy_from];
|
||||
@ -200,19 +215,33 @@ export class EnergyGridSettings extends LitElement {
|
||||
|
||||
private _addFromSource() {
|
||||
showEnergySettingsGridFlowFromDialog(this, {
|
||||
currency: this.preferences.currency,
|
||||
saveCallback: async (source) => {
|
||||
const flowFrom = energySourcesByType(this.preferences).grid![0]
|
||||
.flow_from;
|
||||
saveCallback: async (flow) => {
|
||||
let preferences: EnergyPreferences;
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, flow_from: [...flowFrom, source] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
flow_from: [flow],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, flow_from: [...gridSource.flow_from, flow] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
}
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
@ -220,16 +249,33 @@ export class EnergyGridSettings extends LitElement {
|
||||
|
||||
private _addToSource() {
|
||||
showEnergySettingsGridFlowToDialog(this, {
|
||||
currency: this.preferences.currency,
|
||||
saveCallback: async (source) => {
|
||||
const flowTo = energySourcesByType(this.preferences).grid![0].flow_to;
|
||||
saveCallback: async (flow) => {
|
||||
let preferences: EnergyPreferences;
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid" ? { ...src, flow_to: [...flowTo, source] } : src
|
||||
),
|
||||
};
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
flow_to: [flow],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, flow_to: [...gridSource.flow_to, flow] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
}
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
@ -239,7 +285,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
const origSource: FlowFromGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
showEnergySettingsGridFlowFromDialog(this, {
|
||||
currency: this.preferences.currency,
|
||||
source: { ...origSource },
|
||||
saveCallback: async (source) => {
|
||||
const flowFrom = energySourcesByType(this.preferences).grid![0]
|
||||
@ -267,7 +312,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
const origSource: FlowToGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
showEnergySettingsGridFlowToDialog(this, {
|
||||
currency: this.preferences.currency,
|
||||
source: { ...origSource },
|
||||
saveCallback: async (source) => {
|
||||
const flowTo = energySourcesByType(this.preferences).grid![0].flow_to;
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { showEnergySettingsSolarDialog } from "../dialogs/show-dialogs-energy";
|
||||
import { energyCardStyles } from "./styles";
|
||||
|
||||
@ -37,14 +38,24 @@ export class EnergySolarSettings extends LitElement {
|
||||
return html`
|
||||
<ha-card>
|
||||
<h1 class="card-header">
|
||||
<ha-svg-icon .path=${mdiSolarPower}></ha-svg-icon>Configure solar
|
||||
panels
|
||||
<ha-svg-icon .path=${mdiSolarPower}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.energy.solar.title")}
|
||||
</h1>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Let Home Assistant monitor your solar panels and give you insight on
|
||||
their performace.
|
||||
${this.hass.localize("ui.panel.config.energy.solar.sub")}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/energy/solar-panels/"
|
||||
)}"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.learn_more"
|
||||
)}</a
|
||||
>
|
||||
</p>
|
||||
<h3>Solar production</h3>
|
||||
${solarSources.map((source) => {
|
||||
|
@ -58,12 +58,11 @@ export class DialogEnergyDeviceSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<p>Track your devices <a href="#">Learn more</a></p>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitOfMeasurement=${energyUnits}
|
||||
.label=${`Device production energy (kWh)`}
|
||||
.label=${`Device consumption energy (kWh)`}
|
||||
entities-only
|
||||
@value-changed=${this._statisticChanged}
|
||||
></ha-statistic-picker>
|
||||
|
@ -85,11 +85,11 @@ export class DialogEnergyGridFlowSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<p>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@ -203,7 +203,7 @@ export class DialogEnergyGridFlowSettings
|
||||
<span slot="suffix"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`,
|
||||
{ currency: this._params.currency }
|
||||
{ currency: this.hass.config.currency }
|
||||
)}</span
|
||||
>
|
||||
</paper-input>`
|
||||
@ -212,7 +212,15 @@ export class DialogEnergyGridFlowSettings
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._save} slot="primaryAction">
|
||||
<mwc-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source[
|
||||
this._params!.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
]}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
@ -231,32 +239,42 @@ export class DialogEnergyGridFlowSettings
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: CustomEvent) {
|
||||
this._source!.number_energy_price = Number(ev.detail.value);
|
||||
this._source!.entity_energy_price = null;
|
||||
this._costStat = null;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
number_energy_price: Number(ev.detail.value),
|
||||
entity_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _priceStatChanged(ev: CustomEvent) {
|
||||
this._costStat = ev.detail.value;
|
||||
this._source!.entity_energy_price = null;
|
||||
this._source!.number_energy_price = null;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _priceEntityChanged(ev: CustomEvent) {
|
||||
this._source!.entity_energy_price = ev.detail.value;
|
||||
this._source!.number_energy_price = null;
|
||||
this._costStat = null;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: ev.detail.value,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
this._source![
|
||||
this._params!.direction === "from" ? "stat_energy_from" : "stat_energy_to"
|
||||
] = ev.detail.value;
|
||||
this._source![
|
||||
this._params!.direction === "from"
|
||||
this._source = {
|
||||
...this._source!,
|
||||
[this._params!.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"]: ev.detail.value,
|
||||
[this._params!.direction === "from"
|
||||
? "entity_energy_from"
|
||||
: "entity_energy_to"
|
||||
] = ev.detail.value;
|
||||
: "entity_energy_to"]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@ -277,6 +295,9 @@ export class DialogEnergyGridFlowSettings
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
|
@ -75,7 +75,6 @@ export class DialogEnergySolarSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<p>Solar production for the win! <a href="#">Learn more</a></p>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@ -141,7 +140,11 @@ export class DialogEnergySolarSettings
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._save} slot="primaryAction">
|
||||
<mwc-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source.stat_energy_from}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
@ -192,7 +195,7 @@ export class DialogEnergySolarSettings
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
this._source!.stat_energy_from = ev.detail.value;
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@ -212,6 +215,9 @@ export class DialogEnergySolarSettings
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
img {
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
|
@ -10,7 +10,6 @@ export interface EnergySettingsGridFlowDialogParams {
|
||||
source?:
|
||||
| FlowFromGridSourceEnergyPreference
|
||||
| FlowToGridSourceEnergyPreference;
|
||||
currency: string;
|
||||
direction: "from" | "to";
|
||||
saveCallback: (
|
||||
source:
|
||||
@ -21,13 +20,11 @@ export interface EnergySettingsGridFlowDialogParams {
|
||||
|
||||
export interface EnergySettingsGridFlowFromDialogParams {
|
||||
source?: FlowFromGridSourceEnergyPreference;
|
||||
currency: string;
|
||||
saveCallback: (source: FlowFromGridSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsGridFlowToDialogParams {
|
||||
source?: FlowToGridSourceEnergyPreference;
|
||||
currency: string;
|
||||
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,17 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
EnergyPreferences,
|
||||
getEnergyPreferences,
|
||||
saveEnergyPreferences,
|
||||
} from "../../../data/energy";
|
||||
|
||||
import { EnergyPreferences, getEnergyPreferences } from "../../../data/energy";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./components/ha-energy-device-settings";
|
||||
import "./components/ha-energy-grid-settings";
|
||||
import "./components/ha-energy-solar-settings";
|
||||
import "./components/ha-energy-device-settings";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
const INITIAL_CONFIG = {
|
||||
currency: "€",
|
||||
const INITIAL_CONFIG: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
};
|
||||
@ -72,18 +64,6 @@ class HaConfigEnergy extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.experiences}
|
||||
>
|
||||
<ha-card .header=${"General energy settings"}>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
.label=${"Currency"}
|
||||
.value=${this._preferences!.currency}
|
||||
@value-changed=${this._currencyChanged}
|
||||
>
|
||||
</paper-input>
|
||||
|
||||
<mwc-button @click=${this._save}>Save</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="container">
|
||||
<ha-energy-grid-settings
|
||||
.hass=${this.hass}
|
||||
@ -105,24 +85,6 @@ class HaConfigEnergy extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _currencyChanged(ev: CustomEvent) {
|
||||
this._preferences!.currency = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
if (!this._preferences) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._preferences = await saveEnergyPreferences(
|
||||
this.hass,
|
||||
this._preferences
|
||||
);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchConfig() {
|
||||
try {
|
||||
this._preferences = await getEnergyPreferences(this.hass);
|
||||
|
@ -0,0 +1,273 @@
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiStethoscope, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
} from "../../../../../data/device_registry";
|
||||
import {
|
||||
fetchNetworkStatus,
|
||||
healNode,
|
||||
ZWaveJSNetwork,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSHealNodeDialogParams } from "./show-dialog-zwave_js-heal-node";
|
||||
|
||||
@customElement("dialog-zwave_js-heal-node")
|
||||
class DialogZWaveJSHealNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private entry_id?: string;
|
||||
|
||||
@state() private node_id?: number;
|
||||
|
||||
@state() private device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _status?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public showDialog(params: ZWaveJSHealNodeDialogParams): void {
|
||||
this.entry_id = params.entry_id;
|
||||
this.device = params.device;
|
||||
this.node_id = params.node_id;
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.entry_id = undefined;
|
||||
this._status = undefined;
|
||||
this.node_id = undefined;
|
||||
this.device = undefined;
|
||||
this._error = undefined;
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entry_id || !this.device) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.zwave_js.heal_node.title")
|
||||
)}
|
||||
>
|
||||
${!this._status
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiStethoscope}
|
||||
class="introduction"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.introduction",
|
||||
{
|
||||
device: html`<em
|
||||
>${computeDeviceName(this.device, this.hass!)}</em
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<em>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.traffic_warning"
|
||||
)}
|
||||
</em>
|
||||
</p>
|
||||
<mwc-button slot="primaryAction" @click=${this._startHeal}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.start_heal"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.in_progress",
|
||||
{
|
||||
device: html`<em
|
||||
>${computeDeviceName(this.device, this.hass!)}</em
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.healing_failed",
|
||||
{
|
||||
device: html`<em
|
||||
>${computeDeviceName(this.device, this.hass!)}</em
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this._error
|
||||
? html` <em>${this._error}</em> `
|
||||
: `
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.healing_failed_check_logs"
|
||||
)}
|
||||
`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.healing_complete",
|
||||
{
|
||||
device: html`<em
|
||||
>${computeDeviceName(this.device, this.hass!)}</em
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "network-healing"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.heal_node.network_heal_in_progress"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData(): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const network: ZWaveJSNetwork = await fetchNetworkStatus(
|
||||
this.hass!,
|
||||
this.entry_id!
|
||||
);
|
||||
if (network.controller.is_heal_network_active) {
|
||||
this._status = "network-healing";
|
||||
}
|
||||
}
|
||||
|
||||
private async _startHeal(): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._status = "started";
|
||||
try {
|
||||
this._status = (await healNode(this.hass, this.entry_id!, this.node_id!))
|
||||
? "finished"
|
||||
: "failed";
|
||||
} catch (error) {
|
||||
this._error = error.message;
|
||||
this._status = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
ha-svg-icon.introduction {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.flex-container ha-svg-icon,
|
||||
.flex-container ha-circular-progress {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zwave_js-heal-node": DialogZWaveJSHealNode;
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCheckCircle, mdiCloseCircle, mdiRobotDead } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
removeFailedNode,
|
||||
ZWaveJSRemovedNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSRemoveFailedNodeDialogParams } from "./show-dialog-zwave_js-remove-failed-node";
|
||||
|
||||
@customElement("dialog-zwave_js-remove-failed-node")
|
||||
class DialogZWaveJSRemoveFailedNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private entry_id?: string;
|
||||
|
||||
@state() private node_id?: number;
|
||||
|
||||
@state() private _status = "";
|
||||
|
||||
@state() private _error?: any;
|
||||
|
||||
@state() private _node?: ZWaveJSRemovedNode;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc | void>;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
public async showDialog(
|
||||
params: ZWaveJSRemoveFailedNodeDialogParams
|
||||
): Promise<void> {
|
||||
this.entry_id = params.entry_id;
|
||||
this.node_id = params.node_id;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._unsubscribe();
|
||||
this.entry_id = undefined;
|
||||
this._status = "";
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialogFinished(): void {
|
||||
history.back();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entry_id || !this.node_id) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed="${this.closeDialog}"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.title"
|
||||
)
|
||||
)}
|
||||
>
|
||||
${this._status === ""
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiRobotDead}
|
||||
class="introduction"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.introduction"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this._startExclusion}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.remove_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.in_progress"
|
||||
)}
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="error"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.removal_failed"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
? html` <p><em> ${this._error.message} </em></p> `
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.removal_finished",
|
||||
"id",
|
||||
this._node!.node_id
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialogFinished}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startExclusion(): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._status = "started";
|
||||
this._subscribed = removeFailedNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
(message: any) => this._handleMessage(message)
|
||||
).catch((error) => {
|
||||
this._status = "failed";
|
||||
this._error = error;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.event === "exclusion started") {
|
||||
this._status = "started";
|
||||
}
|
||||
if (message.event === "node removed") {
|
||||
this._status = "finished";
|
||||
this._node = message.node;
|
||||
this._unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private async _unsubscribe(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
const unsubFunc = await this._subscribed;
|
||||
if (unsubFunc instanceof Function) {
|
||||
unsubFunc();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
if (this._status !== "finished") {
|
||||
this._status = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flex-container ha-circular-progress,
|
||||
.flex-container ha-svg-icon {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zwave_js-remove-failed-node": DialogZWaveJSRemoveFailedNode;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||
|
||||
export interface ZWaveJSHealNodeDialogParams {
|
||||
entry_id: string;
|
||||
node_id: number;
|
||||
device: DeviceRegistryEntry;
|
||||
}
|
||||
|
||||
export const loadHealNodeDialog = () => import("./dialog-zwave_js-heal-node");
|
||||
|
||||
export const showZWaveJSHealNodeDialog = (
|
||||
element: HTMLElement,
|
||||
healNodeDialogParams: ZWaveJSHealNodeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zwave_js-heal-node",
|
||||
dialogImport: loadHealNodeDialog,
|
||||
dialogParams: healNodeDialogParams,
|
||||
});
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface ZWaveJSRemoveFailedNodeDialogParams {
|
||||
entry_id: string;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export const loadRemoveFailedNodeDialog = () =>
|
||||
import("./dialog-zwave_js-remove-failed-node");
|
||||
|
||||
export const showZWaveJSRemoveFailedNodeDialog = (
|
||||
element: HTMLElement,
|
||||
removeFailedNodeDialogParams: ZWaveJSRemoveFailedNodeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zwave_js-remove-failed-node",
|
||||
dialogImport: loadRemoveFailedNodeDialog,
|
||||
dialogParams: removeFailedNodeDialogParams,
|
||||
});
|
||||
};
|
@ -131,7 +131,9 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
|
||||
private _downloadLogs() {
|
||||
fileDownload(
|
||||
this,
|
||||
`data:text/plain;charset=utf-8,${encodeURI(this._textarea!.value)}`,
|
||||
`data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
this._textarea!.value
|
||||
)}`,
|
||||
`zwave_js.log`
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { hasTemplate } from "../../../common/string/has-template";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
@ -135,11 +136,15 @@ class HaPanelDevService extends LitElement {
|
||||
>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button .disabled=${!isValid} raised @click=${this._callService}>
|
||||
<ha-progress-button
|
||||
.disabled=${!isValid}
|
||||
raised
|
||||
@click=${this._callService}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.call_service"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -295,7 +300,8 @@ class HaPanelDevService extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private async _callService() {
|
||||
private async _callService(ev) {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
if (!this._serviceData?.service) {
|
||||
return;
|
||||
}
|
||||
@ -310,6 +316,7 @@ class HaPanelDevService extends LitElement {
|
||||
return;
|
||||
}
|
||||
forwardHaptic("failure");
|
||||
button.actionError();
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass.localize(
|
||||
@ -318,7 +325,9 @@ class HaPanelDevService extends LitElement {
|
||||
this._serviceData.service
|
||||
) + ` ${err.message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
button.actionSuccess();
|
||||
}
|
||||
|
||||
private _toggleYaml() {
|
||||
|
@ -20,8 +20,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _step = 0;
|
||||
|
||||
private _preferences: EnergyPreferences = {
|
||||
currency: "€",
|
||||
@state() private _preferences: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
};
|
||||
@ -42,9 +41,6 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>${this.hass.localize("ui.panel.energy.setup.header")}</h2>
|
||||
<h3>${this.hass.localize("ui.panel.energy.setup.slogan")}</h3>
|
||||
|
||||
<p>Step ${this._step + 1} of 3</p>
|
||||
${this._step === 0
|
||||
? html` <ha-energy-grid-settings
|
||||
@ -65,15 +61,15 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
||||
></ha-energy-device-settings>`}
|
||||
<div class="buttons">
|
||||
${this._step > 0
|
||||
? html`<mwc-button @click=${this._back}
|
||||
? html`<mwc-button outlined @click=${this._back}
|
||||
>${this.hass.localize("ui.panel.energy.setup.back")}</mwc-button
|
||||
>`
|
||||
: html`<div></div>`}
|
||||
${this._step < 2
|
||||
? html`<mwc-button outlined @click=${this._next}
|
||||
? html`<mwc-button unelevated @click=${this._next}
|
||||
>${this.hass.localize("ui.panel.energy.setup.next")}</mwc-button
|
||||
>`
|
||||
: html`<mwc-button raised @click=${this._setupDone}>
|
||||
: html`<mwc-button unelevated @click=${this._setupDone}>
|
||||
${this.hass.localize("ui.panel.energy.setup.done")}
|
||||
</mwc-button>`}
|
||||
</div>
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { EnergyPreferences, getEnergyPreferences } from "../../../data/energy";
|
||||
import {
|
||||
EnergyPreferences,
|
||||
getEnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import { LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { LovelaceViewStrategy } from "../../lovelace/strategies/get-strategy";
|
||||
|
||||
@ -39,9 +43,10 @@ export class EnergyStrategy {
|
||||
|
||||
view.type = "sidebar";
|
||||
|
||||
const hasGrid = energyPrefs.energy_sources.some(
|
||||
const hasGrid = energyPrefs.energy_sources.find(
|
||||
(source) => source.type === "grid"
|
||||
);
|
||||
) as GridSourceTypeEnergyPreference;
|
||||
const hasReturn = hasGrid && hasGrid.flow_to.length;
|
||||
const hasSolar = energyPrefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
@ -49,8 +54,8 @@ export class EnergyStrategy {
|
||||
// Only include if we have a grid source.
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
title: "Electricity",
|
||||
type: "energy-summary-graph",
|
||||
title: "Energy usage",
|
||||
type: "energy-usage-graph",
|
||||
prefs: energyPrefs,
|
||||
});
|
||||
}
|
||||
@ -67,30 +72,21 @@ export class EnergyStrategy {
|
||||
// Only include if we have a grid.
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
title: "Costs",
|
||||
type: "energy-costs-table",
|
||||
prefs: energyPrefs,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have at least 1 device in the config.
|
||||
if (energyPrefs.device_consumption.length) {
|
||||
view.cards!.push({
|
||||
title: "Monitor individual devices",
|
||||
type: "energy-devices-graph",
|
||||
prefs: energyPrefs,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid.
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
type: "energy-usage",
|
||||
title: "Energy distribution",
|
||||
type: "energy-distribution",
|
||||
prefs: energyPrefs,
|
||||
view_layout: { position: "sidebar" },
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar) {
|
||||
view.cards!.push({
|
||||
title: "Sources",
|
||||
type: "energy-sources-table",
|
||||
prefs: energyPrefs,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
view.cards!.push({
|
||||
@ -100,6 +96,15 @@ export class EnergyStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
view.cards!.push({
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
prefs: energyPrefs,
|
||||
view_layout: { position: "sidebar" },
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
@ -109,11 +114,14 @@ export class EnergyStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
view.cards!.push({
|
||||
type: "energy-summary",
|
||||
prefs: energyPrefs,
|
||||
view_layout: { position: "sidebar" },
|
||||
});
|
||||
// Only include if we have at least 1 device in the config.
|
||||
if (energyPrefs.device_consumption.length) {
|
||||
view.cards!.push({
|
||||
title: "Monitor individual devices",
|
||||
type: "energy-devices-graph",
|
||||
prefs: energyPrefs,
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { round } from "../../../common/number/round";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-gauge";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import { energySourcesByType } from "../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { round } from "../../../../common/number/round";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-gauge";
|
||||
import { getConfigEntries } from "../../../../data/config_entries";
|
||||
import { energySourcesByType } from "../../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCard } from "../types";
|
||||
import { severityMap } from "./hui-gauge-card";
|
||||
import type { EnergyCarbonGaugeCardConfig } from "./types";
|
||||
} from "../../../../data/history";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { createEntityNotFoundWarning } from "../../components/hui-warning";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import { severityMap } from "../hui-gauge-card";
|
||||
import type { EnergyCarbonGaugeCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-energy-carbon-consumed-gauge-card")
|
||||
class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
@ -41,7 +43,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._getStatistics();
|
||||
this._fetchCO2SignalEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,24 +51,20 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats || this._co2SignalEntity === undefined) {
|
||||
return html`Loading...`;
|
||||
if (this._co2SignalEntity === null) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._co2SignalEntity) {
|
||||
return html``;
|
||||
if (!this._stats || !this._co2SignalEntity) {
|
||||
return html`Loading...`;
|
||||
}
|
||||
|
||||
const co2State = this.hass.states[this._co2SignalEntity];
|
||||
|
||||
if (!co2State) {
|
||||
return html`No CO2 Signal entity found.`;
|
||||
}
|
||||
|
||||
const co2percentage = Number(co2State.state);
|
||||
|
||||
if (isNaN(co2percentage)) {
|
||||
return html``;
|
||||
return html`<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._co2SignalEntity)}
|
||||
</hui-warning>`;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
@ -78,56 +75,65 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
|
||||
);
|
||||
|
||||
const totalSolarProduction = types.solar
|
||||
? calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.solar.map((source) => source.stat_energy_from)
|
||||
)
|
||||
: undefined;
|
||||
let value: number | undefined;
|
||||
|
||||
const totalGridReturned = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||
);
|
||||
if (this._co2SignalEntity in this._stats && totalGridConsumption) {
|
||||
const highCarbonEnergy =
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
this._stats[this._co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._stats![flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
) || 0;
|
||||
|
||||
if (totalGridConsumption === null) {
|
||||
return html`Couldn't calculate the total grid consumption.`;
|
||||
const totalSolarProduction = types.solar
|
||||
? calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.solar.map((source) => source.stat_energy_from)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const totalGridReturned = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||
);
|
||||
|
||||
const totalEnergyConsumed =
|
||||
totalGridConsumption +
|
||||
Math.max(0, (totalSolarProduction || 0) - (totalGridReturned || 0));
|
||||
|
||||
value = round((1 - highCarbonEnergy / totalEnergyConsumed) * 100);
|
||||
}
|
||||
|
||||
const highCarbonEnergy = (totalGridConsumption * co2percentage) / 100;
|
||||
|
||||
const totalEnergyConsumed =
|
||||
totalGridConsumption +
|
||||
(totalSolarProduction || 0) -
|
||||
(totalGridReturned || 0);
|
||||
|
||||
const value = round((highCarbonEnergy / totalEnergyConsumed) * 100);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-gauge
|
||||
min="0"
|
||||
max="100"
|
||||
.value=${value}
|
||||
.locale=${this.hass!.locale}
|
||||
label="%"
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(64),
|
||||
})}
|
||||
></ha-gauge>
|
||||
<div class="name">High-carbon energy consumed</div>
|
||||
<ha-card
|
||||
>${value !== undefined
|
||||
? html` <ha-gauge
|
||||
min="0"
|
||||
max="100"
|
||||
.value=${value}
|
||||
.locale=${this.hass!.locale}
|
||||
label="%"
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(value),
|
||||
})}
|
||||
></ha-gauge>
|
||||
<div class="name">Non-fossil energy consumed</div>`
|
||||
: html`Consumed non-fossil energy couldn't be calculated`}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeSeverity(numberValue: number): string {
|
||||
if (numberValue > 50) {
|
||||
if (numberValue < 10) {
|
||||
return severityMap.red;
|
||||
}
|
||||
if (numberValue > 30) {
|
||||
if (numberValue < 30) {
|
||||
return severityMap.yellow;
|
||||
}
|
||||
if (numberValue < 10) {
|
||||
if (numberValue > 75) {
|
||||
return severityMap.green;
|
||||
}
|
||||
return severityMap.normal;
|
||||
@ -166,6 +172,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
await this._fetchCO2SignalEntity();
|
||||
|
||||
if (this._co2SignalEntity === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
@ -187,6 +199,10 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._co2SignalEntity) {
|
||||
statistics.push(this._co2SignalEntity);
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
@ -14,18 +14,22 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { getColorByIndex } from "../../../common/color/colors";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-card";
|
||||
import { getColorByIndex } from "../../../../common/color/colors";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/string/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergyDevicesGraphCardConfig } from "./types";
|
||||
} from "../../../../data/history";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyDevicesGraphCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-energy-devices-graph-card")
|
||||
export class HuiEnergyDevicesGraphCard
|
||||
@ -106,7 +110,10 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header="${this._config.title}">
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
@ -138,18 +145,21 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: { bar: { borderWidth: 1.5 } },
|
||||
elements: { bar: { borderWidth: 1.5, borderRadius: 4 } },
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: "nearest",
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${
|
||||
Math.round(context.parsed.x * 100) / 100
|
||||
} kWh`,
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.x,
|
||||
this.hass.locale
|
||||
)} kWh`,
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
|
||||
@ -178,10 +188,6 @@ export class HuiEnergyDevicesGraphCard
|
||||
const statisticsData = Object.values(this._data!);
|
||||
let endTime: Date;
|
||||
|
||||
if (statisticsData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
endTime = new Date(
|
||||
Math.max(
|
||||
...statisticsData.map((stats) =>
|
||||
@ -190,7 +196,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
)
|
||||
);
|
||||
|
||||
if (endTime > new Date()) {
|
||||
if (!endTime || endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
@ -207,27 +213,30 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
];
|
||||
|
||||
Object.entries(this._data).forEach(([id, statistics], idx) => {
|
||||
const entity = this.hass.states[id];
|
||||
const label = entity ? computeStateName(entity) : id;
|
||||
for (let idx = 0; idx < prefs.device_consumption.length; idx++) {
|
||||
const device = prefs.device_consumption[idx];
|
||||
const entity = this.hass.states[device.stat_consumption];
|
||||
const label = entity ? computeStateName(entity) : device.stat_consumption;
|
||||
|
||||
const color = getColorByIndex(idx);
|
||||
|
||||
borderColor.push(color);
|
||||
backgroundColor.push(color + "7F");
|
||||
|
||||
const value = calculateStatisticSumGrowth(statistics);
|
||||
const value =
|
||||
device.stat_consumption in this._data
|
||||
? calculateStatisticSumGrowth(this._data[device.stat_consumption])
|
||||
: 0;
|
||||
data.push({
|
||||
// @ts-expect-error
|
||||
y: label,
|
||||
x: value || 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
data.sort((a, b) => b.x - a.x);
|
||||
|
||||
this._chartData = {
|
||||
// labels,
|
||||
datasets,
|
||||
};
|
||||
}
|
||||
@ -237,6 +246,9 @@ export class HuiEnergyDevicesGraphCard
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
625
src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts
Normal file
625
src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts
Normal file
@ -0,0 +1,625 @@
|
||||
import {
|
||||
mdiArrowLeft,
|
||||
mdiArrowRight,
|
||||
mdiHome,
|
||||
mdiLeaf,
|
||||
mdiSolarPower,
|
||||
mdiTransmissionTower,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { formatNumber } from "../../../../common/string/format_number";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { getConfigEntries } from "../../../../data/config_entries";
|
||||
import { energySourcesByType } from "../../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../../data/history";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyDistributionCardConfig } from "../types";
|
||||
|
||||
const CIRCLE_CIRCUMFERENCE = 238.76104;
|
||||
|
||||
@customElement("hui-energy-distribution-card")
|
||||
class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergyDistributionCardConfig;
|
||||
|
||||
@state() private _stats?: Statistics;
|
||||
|
||||
@state() private _co2SignalEntity?: string;
|
||||
|
||||
private _fetching = false;
|
||||
|
||||
public setConfig(config: EnergyDistributionCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._fetching && !this._stats) {
|
||||
this._fetching = true;
|
||||
this._getStatistics().then(() => {
|
||||
this._fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats) {
|
||||
return html`Loading…`;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
// The strategy only includes this card if we have a grid.
|
||||
const hasConsumption = true;
|
||||
|
||||
const hasSolarProduction = types.solar !== undefined;
|
||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||
|
||||
const totalGridConsumption =
|
||||
calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
|
||||
) ?? 0;
|
||||
|
||||
let totalSolarProduction: number | null = null;
|
||||
|
||||
if (hasSolarProduction) {
|
||||
totalSolarProduction =
|
||||
calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.solar!.map((source) => source.stat_energy_from)
|
||||
) || 0;
|
||||
}
|
||||
|
||||
let productionReturnedToGrid: number | null = null;
|
||||
|
||||
if (hasReturnToGrid) {
|
||||
productionReturnedToGrid =
|
||||
calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||
) || 0;
|
||||
}
|
||||
|
||||
const solarConsumption = Math.max(
|
||||
0,
|
||||
(totalSolarProduction || 0) - (productionReturnedToGrid || 0)
|
||||
);
|
||||
|
||||
const totalHomeConsumption = totalGridConsumption + solarConsumption;
|
||||
|
||||
let homeSolarCircumference: number | undefined;
|
||||
if (hasSolarProduction) {
|
||||
homeSolarCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * (solarConsumption / totalHomeConsumption);
|
||||
}
|
||||
|
||||
let lowCarbonConsumption: number | undefined;
|
||||
|
||||
let homeLowCarbonCircumference: number | undefined;
|
||||
let homeHighCarbonCircumference: number | undefined;
|
||||
|
||||
let electricityMapUrl: string | undefined;
|
||||
|
||||
if (this._co2SignalEntity && this._co2SignalEntity in this._stats) {
|
||||
// Calculate high carbon consumption
|
||||
const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage(
|
||||
this._stats[this._co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map((flow) => this._stats![flow.stat_energy_from])
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const co2State = this.hass.states[this._co2SignalEntity];
|
||||
|
||||
if (co2State) {
|
||||
electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`;
|
||||
}
|
||||
|
||||
if (highCarbonConsumption !== null) {
|
||||
lowCarbonConsumption = totalGridConsumption - highCarbonConsumption;
|
||||
|
||||
homeHighCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption);
|
||||
|
||||
homeLowCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
(homeSolarCircumference || 0) -
|
||||
homeHighCarbonCircumference;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div class="card-content">
|
||||
${lowCarbonConsumption !== undefined || hasSolarProduction
|
||||
? html`<div class="row">
|
||||
${lowCarbonConsumption === undefined
|
||||
? html`<div class="spacer"></div>`
|
||||
: html`<div class="circle-container low-carbon">
|
||||
<span class="label">Non-fossil</span>
|
||||
<a
|
||||
class="circle"
|
||||
href=${ifDefined(electricityMapUrl)}
|
||||
target="_blank"
|
||||
rel="noopener no referrer"
|
||||
>
|
||||
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
|
||||
${lowCarbonConsumption
|
||||
? formatNumber(
|
||||
lowCarbonConsumption,
|
||||
this.hass.locale,
|
||||
{ maximumFractionDigits: 1 }
|
||||
)
|
||||
: "-"}
|
||||
kWh
|
||||
</a>
|
||||
<svg width="80" height="30">
|
||||
<line x1="40" y1="0" x2="40" y2="30"></line>
|
||||
</svg>
|
||||
</div>`}
|
||||
${hasSolarProduction
|
||||
? html`<div class="circle-container solar">
|
||||
<span class="label">Solar</span>
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path="${mdiSolarPower}"></ha-svg-icon>
|
||||
${formatNumber(
|
||||
totalSolarProduction || 0,
|
||||
this.hass.locale,
|
||||
{ maximumFractionDigits: 1 }
|
||||
)}
|
||||
kWh
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="spacer"></div>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<div class="circle-container grid">
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon>
|
||||
<span class="consumption">
|
||||
${hasReturnToGrid
|
||||
? html`<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowRight}
|
||||
></ha-svg-icon>`
|
||||
: ""}${formatNumber(
|
||||
totalGridConsumption,
|
||||
this.hass.locale,
|
||||
{ maximumFractionDigits: 1 }
|
||||
)}
|
||||
kWh
|
||||
</span>
|
||||
${productionReturnedToGrid !== null
|
||||
? html`<span class="return">
|
||||
<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowLeft}
|
||||
></ha-svg-icon
|
||||
>${formatNumber(
|
||||
productionReturnedToGrid,
|
||||
this.hass.locale,
|
||||
{ maximumFractionDigits: 1 }
|
||||
)}
|
||||
kWh
|
||||
</span>`
|
||||
: ""}
|
||||
</div>
|
||||
<span class="label">Grid</span>
|
||||
</div>
|
||||
<div class="circle-container home">
|
||||
<div
|
||||
class="circle ${classMap({
|
||||
border:
|
||||
homeSolarCircumference === undefined &&
|
||||
homeLowCarbonCircumference === undefined,
|
||||
})}"
|
||||
>
|
||||
<ha-svg-icon .path="${mdiHome}"></ha-svg-icon>
|
||||
${formatNumber(totalHomeConsumption, this.hass.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
kWh
|
||||
${homeSolarCircumference !== undefined ||
|
||||
homeLowCarbonCircumference !== undefined
|
||||
? html`<svg>
|
||||
${homeSolarCircumference !== undefined
|
||||
? svg`<circle
|
||||
class="solar"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="38"
|
||||
stroke-dasharray="${homeSolarCircumference} ${
|
||||
CIRCLE_CIRCUMFERENCE - homeSolarCircumference
|
||||
}"
|
||||
shape-rendering="geometricPrecision"
|
||||
stroke-dashoffset="-${
|
||||
CIRCLE_CIRCUMFERENCE - homeSolarCircumference
|
||||
}"
|
||||
/>`
|
||||
: ""}
|
||||
${homeLowCarbonCircumference
|
||||
? svg`<circle
|
||||
class="low-carbon"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="38"
|
||||
stroke-dasharray="${homeLowCarbonCircumference} ${
|
||||
CIRCLE_CIRCUMFERENCE - homeLowCarbonCircumference
|
||||
}"
|
||||
stroke-dashoffset="-${
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
homeLowCarbonCircumference -
|
||||
(homeSolarCircumference || 0)
|
||||
}"
|
||||
shape-rendering="geometricPrecision"
|
||||
/>`
|
||||
: ""}
|
||||
<circle
|
||||
class="grid"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="38"
|
||||
stroke-dasharray="${homeHighCarbonCircumference ??
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
homeSolarCircumference!} ${homeHighCarbonCircumference !==
|
||||
undefined
|
||||
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
|
||||
: homeSolarCircumference}"
|
||||
stroke-dashoffset="0"
|
||||
shape-rendering="geometricPrecision"
|
||||
/>
|
||||
</svg>`
|
||||
: ""}
|
||||
</div>
|
||||
<span class="label">Home</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lines">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
${hasReturnToGrid && hasSolarProduction
|
||||
? svg`<path
|
||||
id="return"
|
||||
class="return"
|
||||
d="M47,0 v15 c0,40 -10,35 -30,35 h-20"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path> `
|
||||
: ""}
|
||||
${hasSolarProduction
|
||||
? svg`<path
|
||||
id="solar"
|
||||
class="solar"
|
||||
d="M${
|
||||
hasReturnToGrid ? 53 : 50
|
||||
},0 v15 c0,40 10,35 30,35 h20"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>`
|
||||
: ""}
|
||||
<path
|
||||
class="grid"
|
||||
id="grid"
|
||||
d="M0,${hasSolarProduction ? 56 : 53} H100"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>
|
||||
${productionReturnedToGrid && hasSolarProduction
|
||||
? svg`<circle
|
||||
r="1"
|
||||
class="return"
|
||||
vector-effect="non-scaling-stroke"
|
||||
>
|
||||
<animateMotion
|
||||
dur="${
|
||||
6 -
|
||||
(productionReturnedToGrid /
|
||||
(totalGridConsumption +
|
||||
(totalSolarProduction || 0))) *
|
||||
5
|
||||
}s"
|
||||
repeatCount="indefinite"
|
||||
rotate="auto"
|
||||
>
|
||||
<mpath xlink:href="#return" />
|
||||
</animateMotion>
|
||||
</circle>`
|
||||
: ""}
|
||||
${totalSolarProduction
|
||||
? svg`<circle
|
||||
r="1"
|
||||
class="solar"
|
||||
vector-effect="non-scaling-stroke"
|
||||
>
|
||||
<animateMotion
|
||||
dur="${
|
||||
6 -
|
||||
((totalSolarProduction -
|
||||
(productionReturnedToGrid || 0)) /
|
||||
(totalGridConsumption +
|
||||
(totalSolarProduction || 0))) *
|
||||
5
|
||||
}s"
|
||||
repeatCount="indefinite"
|
||||
rotate="auto"
|
||||
>
|
||||
<mpath xlink:href="#solar" />
|
||||
</animateMotion>
|
||||
</circle>`
|
||||
: ""}
|
||||
${totalGridConsumption
|
||||
? svg`<circle
|
||||
r="1"
|
||||
class="grid"
|
||||
vector-effect="non-scaling-stroke"
|
||||
>
|
||||
<animateMotion
|
||||
dur="${
|
||||
6 -
|
||||
(totalGridConsumption /
|
||||
(totalGridConsumption +
|
||||
(totalSolarProduction || 0))) *
|
||||
5
|
||||
}s"
|
||||
repeatCount="indefinite"
|
||||
rotate="auto"
|
||||
>
|
||||
<mpath xlink:href="#grid" />
|
||||
</animateMotion>
|
||||
</circle>`
|
||||
: ""}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const [configEntries, entityRegistryEntries] = await Promise.all([
|
||||
getConfigEntries(this.hass),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry),
|
||||
]);
|
||||
|
||||
const co2ConfigEntry = configEntries.find(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
|
||||
this._co2SignalEntity = undefined;
|
||||
|
||||
if (co2ConfigEntry) {
|
||||
for (const entry of entityRegistryEntries) {
|
||||
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The integration offers 2 entities. We want the % one.
|
||||
const co2State = this.hass.states[entry.entity_id];
|
||||
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._co2SignalEntity = co2State.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = [];
|
||||
|
||||
if (this._co2SignalEntity !== undefined) {
|
||||
statistics.push(this._co2SignalEntity);
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
statistics.push(source.stat_energy_from);
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
}
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.card-content {
|
||||
position: relative;
|
||||
}
|
||||
.lines {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 16px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.lines svg {
|
||||
width: calc(100% - 160px);
|
||||
height: 100%;
|
||||
max-width: 340px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.circle-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.circle-container.low-carbon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.circle-container.solar {
|
||||
margin-left: 4px;
|
||||
height: 130px;
|
||||
}
|
||||
.spacer {
|
||||
width: 84px;
|
||||
}
|
||||
.circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
ha-svg-icon.small {
|
||||
--mdc-icon-size: 12px;
|
||||
}
|
||||
.label {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
line,
|
||||
path {
|
||||
stroke: var(--primary-text-color);
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
.circle svg {
|
||||
position: absolute;
|
||||
fill: none;
|
||||
stroke-width: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.low-carbon line {
|
||||
stroke: var(--energy-non-fossil-color);
|
||||
}
|
||||
.low-carbon .circle {
|
||||
border-color: var(--energy-non-fossil-color);
|
||||
}
|
||||
.low-carbon ha-svg-icon {
|
||||
color: var(--energy-non-fossil-color);
|
||||
}
|
||||
circle.low-carbon {
|
||||
stroke: var(--energy-non-fossil-color);
|
||||
fill: var(--energy-non-fossil-color);
|
||||
}
|
||||
.solar .circle {
|
||||
border-color: var(--energy-solar-color);
|
||||
}
|
||||
circle.solar,
|
||||
path.solar {
|
||||
stroke: var(--energy-solar-color);
|
||||
}
|
||||
circle.solar {
|
||||
stroke-width: 4;
|
||||
fill: var(--energy-solar-color);
|
||||
}
|
||||
path.return,
|
||||
circle.return {
|
||||
stroke: var(--energy-grid-return-color);
|
||||
}
|
||||
circle.return {
|
||||
stroke-width: 4;
|
||||
fill: var(--energy-grid-return-color);
|
||||
}
|
||||
.return {
|
||||
color: var(--energy-grid-return-color);
|
||||
}
|
||||
.grid .circle {
|
||||
border-color: var(--energy-grid-consumption-color);
|
||||
}
|
||||
.consumption {
|
||||
color: var(--energy-grid-consumption-color);
|
||||
}
|
||||
circle.grid,
|
||||
path.grid {
|
||||
stroke: var(--energy-grid-consumption-color);
|
||||
}
|
||||
circle.grid {
|
||||
stroke-width: 4;
|
||||
fill: var(--energy-grid-consumption-color);
|
||||
}
|
||||
.home .circle {
|
||||
border-width: 0;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.home .circle.border {
|
||||
border-width: 2px;
|
||||
}
|
||||
.circle svg circle {
|
||||
animation: rotate-in 0.6s ease-in;
|
||||
transition: stroke-dashoffset 0.4s, stroke-dasharray 0.4s;
|
||||
fill: none;
|
||||
}
|
||||
@keyframes rotate-in {
|
||||
from {
|
||||
stroke-dashoffset: 238.76104;
|
||||
stroke-dasharray: 238.76104;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-distribution-card": HuiEnergyDistrubutionCard;
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatNumber } from "../../../../common/string/format_number";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-gauge";
|
||||
import type { LevelDefinition } from "../../../../components/ha-gauge";
|
||||
import { GridSourceTypeEnergyPreference } from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../../data/history";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyGridGaugeCardConfig } from "../types";
|
||||
|
||||
const LEVELS: LevelDefinition[] = [
|
||||
{ level: -1, stroke: "var(--label-badge-red)" },
|
||||
{ level: -0.2, stroke: "var(--label-badge-yellow)" },
|
||||
{ level: 0, stroke: "var(--label-badge-green)" },
|
||||
];
|
||||
|
||||
@customElement("hui-energy-grid-neutrality-gauge-card")
|
||||
class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergyGridGaugeCardConfig;
|
||||
|
||||
@state() private _stats?: Statistics;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergyGridGaugeCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._getStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats) {
|
||||
return html`Loading...`;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
const gridSource = prefs.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (!gridSource) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const consumedFromGrid = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
gridSource.flow_from.map((flow) => flow.stat_energy_from)
|
||||
);
|
||||
|
||||
const returnedToGrid = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
gridSource.flow_to.map((flow) => flow.stat_energy_to)
|
||||
);
|
||||
|
||||
if (consumedFromGrid !== null && returnedToGrid !== null) {
|
||||
if (returnedToGrid > consumedFromGrid) {
|
||||
value = 1 - consumedFromGrid / returnedToGrid;
|
||||
} else if (returnedToGrid < consumedFromGrid) {
|
||||
value = (1 - returnedToGrid / consumedFromGrid) * -1;
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${value !== undefined
|
||||
? html`<ha-gauge
|
||||
min="-1"
|
||||
max="1"
|
||||
.value=${value}
|
||||
.valueText=${formatNumber(
|
||||
Math.abs(returnedToGrid! - consumedFromGrid!),
|
||||
this.hass.locale,
|
||||
{ maximumFractionDigits: 2 }
|
||||
)}
|
||||
.locale=${this.hass!.locale}
|
||||
.levels=${LEVELS}
|
||||
label="kWh"
|
||||
needle
|
||||
></ha-gauge>
|
||||
<div class="name">
|
||||
${returnedToGrid! >= consumedFromGrid!
|
||||
? "Returned to the grid"
|
||||
: "Consumed from the grid"}
|
||||
</div>`
|
||||
: "Grid neutrality could not be calculated"}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = [];
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
}
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ha-gauge {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
line-height: initial;
|
||||
color: var(--primary-text-color);
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-grid-neutrality-gauge-card": HuiEnergyGridGaugeCard;
|
||||
}
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { round } from "../../../common/number/round";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-gauge";
|
||||
import { energySourcesByType } from "../../../data/energy";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-gauge";
|
||||
import { energySourcesByType } from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCard } from "../types";
|
||||
import { severityMap } from "./hui-gauge-card";
|
||||
import type { EnergySolarGaugeCardConfig } from "./types";
|
||||
} from "../../../../data/history";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import { severityMap } from "../hui-gauge-card";
|
||||
import type { EnergySolarGaugeCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-energy-solar-consumed-gauge-card")
|
||||
class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
|
||||
@ -63,33 +62,42 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (productionReturnedToGrid !== null && totalSolarProduction !== null) {
|
||||
const cosumedSolar = totalSolarProduction - productionReturnedToGrid;
|
||||
value = round((cosumedSolar / totalSolarProduction) * 100);
|
||||
if (productionReturnedToGrid !== null && totalSolarProduction) {
|
||||
const cosumedSolar = Math.max(
|
||||
0,
|
||||
totalSolarProduction - productionReturnedToGrid
|
||||
);
|
||||
value = (cosumedSolar / totalSolarProduction) * 100;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${value
|
||||
? html` <ha-gauge
|
||||
${value !== undefined
|
||||
? html`<ha-gauge
|
||||
min="0"
|
||||
max="100"
|
||||
.value=${value}
|
||||
.locale=${this.hass!.locale}
|
||||
label="%"
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(64),
|
||||
"--gauge-color": this._computeSeverity(value),
|
||||
})}
|
||||
></ha-gauge>
|
||||
<div class="name">Self consumed solar energy</div>`
|
||||
: html`Self consumed solar energy couldn't be calculated`}
|
||||
: totalSolarProduction === 0
|
||||
? "You have not produced any solar energy"
|
||||
: "Self consumed solar energy couldn't be calculated"}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeSeverity(numberValue: number): string {
|
||||
if (numberValue > 50) {
|
||||
if (numberValue > 75) {
|
||||
return severityMap.green;
|
||||
}
|
||||
if (numberValue < 50) {
|
||||
return severityMap.yellow;
|
||||
}
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
@ -8,31 +8,33 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../../components/ha-card";
|
||||
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergySolarGraphCardConfig } from "./types";
|
||||
import { fetchStatistics, Statistics } from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergySolarGraphCardConfig } from "../types";
|
||||
import { fetchStatistics, Statistics } from "../../../../data/history";
|
||||
import {
|
||||
hex2rgb,
|
||||
lab2rgb,
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
} from "../../../common/color/convert-color";
|
||||
import { labDarken } from "../../../common/color/lab";
|
||||
import { SolarSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labDarken } from "../../../../common/color/lab";
|
||||
import { SolarSourceTypeEnergyPreference } from "../../../../data/energy";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import {
|
||||
ForecastSolarForecast,
|
||||
getForecastSolarForecasts,
|
||||
} from "../../../data/forecast_solar";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-formfield";
|
||||
|
||||
const SOLAR_COLOR = { border: "#FF9800", background: "#ffcb80" };
|
||||
} from "../../../../data/forecast_solar";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-formfield";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/string/format_number";
|
||||
|
||||
@customElement("hui-energy-solar-graph-card")
|
||||
export class HuiEnergySolarGraphCard
|
||||
@ -45,7 +47,9 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
@state() private _data?: Statistics;
|
||||
|
||||
@state() private _chartData?: ChartData;
|
||||
@state() private _chartData: ChartData = {
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
@state() private _forecasts?: Record<string, ForecastSolarForecast>;
|
||||
|
||||
@ -117,37 +121,38 @@ export class HuiEnergySolarGraphCard
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header="${this._config.title}">
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
})}"
|
||||
>
|
||||
<ha-formfield label="Show all forecast data"
|
||||
><ha-switch
|
||||
.checked=${this._showAllForecastData}
|
||||
@change=${this._showAllForecastChanged}
|
||||
></ha-switch
|
||||
></ha-formfield>
|
||||
${this._chartData
|
||||
? html`<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
chart-type="line"
|
||||
></ha-chart-base>`
|
||||
: ""}
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
chart-type="bar"
|
||||
></ha-chart-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createOptions() {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const startTime = startDate.getTime();
|
||||
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: startTime,
|
||||
suggestedMax: startTime + 24 * 60 * 60 * 1000,
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
@ -166,11 +171,16 @@ export class HuiEnergySolarGraphCard
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetimeseconds",
|
||||
tooltipFormat: "datetime",
|
||||
},
|
||||
offset: true,
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: "kWh",
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
@ -181,7 +191,10 @@ export class HuiEnergySolarGraphCard
|
||||
mode: "nearest",
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${context.parsed.y} kWh`,
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale
|
||||
)} kWh`,
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
@ -199,13 +212,16 @@ export class HuiEnergySolarGraphCard
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4,
|
||||
tension: 0.3,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 5,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
|
||||
@ -213,6 +229,7 @@ export class HuiEnergySolarGraphCard
|
||||
if (this._fetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
@ -252,13 +269,9 @@ export class HuiEnergySolarGraphCard
|
||||
) as SolarSourceTypeEnergyPreference[];
|
||||
|
||||
const statisticsData = Object.values(this._data!);
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
const datasets: ChartDataset<"bar">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (statisticsData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
endTime = new Date(
|
||||
Math.max(
|
||||
...statisticsData.map((stats) =>
|
||||
@ -267,28 +280,29 @@ export class HuiEnergySolarGraphCard
|
||||
)
|
||||
);
|
||||
|
||||
if (endTime > new Date()) {
|
||||
if (!endTime || endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const solarColor = computedStyles
|
||||
.getPropertyValue("--energy-solar-color")
|
||||
.trim();
|
||||
|
||||
solarSources.forEach((source, idx) => {
|
||||
const data: ChartDataset<"line">[] = [];
|
||||
const data: ChartDataset<"bar" | "line">[] = [];
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
|
||||
const borderColor =
|
||||
idx > 0
|
||||
? rgb2hex(
|
||||
lab2rgb(labDarken(rgb2lab(hex2rgb(SOLAR_COLOR.border)), idx))
|
||||
)
|
||||
: SOLAR_COLOR.border;
|
||||
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)))
|
||||
: solarColor;
|
||||
|
||||
data.push({
|
||||
label: `Production ${
|
||||
entity ? computeStateName(entity) : source.stat_energy_from
|
||||
}`,
|
||||
fill: true,
|
||||
stepped: false,
|
||||
borderColor: borderColor,
|
||||
borderColor,
|
||||
backgroundColor: borderColor + "7F",
|
||||
data: [],
|
||||
});
|
||||
@ -309,7 +323,7 @@ export class HuiEnergySolarGraphCard
|
||||
if (prevStart === point.start) {
|
||||
continue;
|
||||
}
|
||||
const value = Math.round((point.sum - prevValue) * 100) / 100;
|
||||
const value = point.sum - prevValue;
|
||||
const date = new Date(point.start);
|
||||
data[0].data.push({
|
||||
x: date.getTime(),
|
||||
@ -343,12 +357,15 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
if (forecastsData) {
|
||||
const forecast: ChartDataset<"line"> = {
|
||||
type: "line",
|
||||
label: `Forecast ${
|
||||
entity ? computeStateName(entity) : source.stat_energy_from
|
||||
}`,
|
||||
fill: false,
|
||||
stepped: false,
|
||||
borderColor: "#000",
|
||||
borderColor: computedStyles.getPropertyValue(
|
||||
"--primary-text-color"
|
||||
),
|
||||
borderDash: [7, 5],
|
||||
pointRadius: 0,
|
||||
data: [],
|
||||
@ -382,16 +399,14 @@ export class HuiEnergySolarGraphCard
|
||||
};
|
||||
}
|
||||
|
||||
private _showAllForecastChanged(ev) {
|
||||
this._showAllForecastData = ev.target.checked;
|
||||
this._renderChart();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
@ -0,0 +1,427 @@
|
||||
// @ts-ignore
|
||||
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import {
|
||||
rgb2hex,
|
||||
lab2rgb,
|
||||
rgb2lab,
|
||||
hex2rgb,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labDarken } from "../../../../common/color/lab";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../../../common/string/format_number";
|
||||
import "../../../../components/chart/statistics-chart";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
EnergyInfo,
|
||||
energySourcesByType,
|
||||
getEnergyInfo,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../../data/history";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergySourcesTableCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-energy-sources-table-card")
|
||||
export class HuiEnergySourcesTableCard
|
||||
extends LitElement
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergySourcesTableCardConfig;
|
||||
|
||||
@state() private _stats?: Statistics;
|
||||
|
||||
@state() private _energyInfo?: EnergyInfo;
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergySourcesTableCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this._getEnergyInfo().then(() => this._getStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats) {
|
||||
return html`Loading...`;
|
||||
}
|
||||
|
||||
let totalGrid = 0;
|
||||
let totalSolar = 0;
|
||||
let totalCost = 0;
|
||||
|
||||
const types = energySourcesByType(this._config.prefs);
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const solarColor = computedStyles
|
||||
.getPropertyValue("--energy-solar-color")
|
||||
.trim();
|
||||
const returnColor = computedStyles
|
||||
.getPropertyValue("--energy-grid-return-color")
|
||||
.trim();
|
||||
const consumptionColor = computedStyles
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim();
|
||||
|
||||
const showCosts =
|
||||
types.grid?.[0].flow_from.some(
|
||||
(flow) =>
|
||||
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
|
||||
) ||
|
||||
types.grid?.[0].flow_to.some(
|
||||
(flow) =>
|
||||
flow.stat_compensation ||
|
||||
flow.entity_energy_price ||
|
||||
flow.number_energy_price
|
||||
);
|
||||
|
||||
return html` <ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div class="mdc-data-table">
|
||||
<div class="mdc-data-table__table-container">
|
||||
<table class="mdc-data-table__table" aria-label="Dessert calories">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
<th class="mdc-data-table__header-cell"></th>
|
||||
<th
|
||||
class="mdc-data-table__header-cell"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Source
|
||||
</th>
|
||||
<th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Energy
|
||||
</th>
|
||||
${showCosts
|
||||
? html` <th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Cost
|
||||
</th>`
|
||||
: ""}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="mdc-data-table__content">
|
||||
${types.solar?.map((source, idx) => {
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
const energy =
|
||||
calculateStatisticSumGrowth(
|
||||
this._stats![source.stat_energy_from]
|
||||
) || 0;
|
||||
totalSolar += energy;
|
||||
const color =
|
||||
idx > 0
|
||||
? rgb2hex(
|
||||
lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))
|
||||
)
|
||||
: solarColor;
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
})}
|
||||
></div>
|
||||
</td>
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
${entity
|
||||
? computeStateName(entity)
|
||||
: source.stat_energy_from}
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(energy, this.hass.locale)} kWh
|
||||
</td>
|
||||
${showCosts
|
||||
? html`<td class="mdc-data-table__cell"></td>`
|
||||
: ""}
|
||||
</tr>`;
|
||||
})}
|
||||
${types.solar
|
||||
? html`<tr class="mdc-data-table__row total">
|
||||
<td class="mdc-data-table__cell"></td>
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
Solar total
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(totalSolar, this.hass.locale)} kWh
|
||||
</td>
|
||||
${showCosts
|
||||
? html`<td class="mdc-data-table__cell"></td>`
|
||||
: ""}
|
||||
</tr>`
|
||||
: ""}
|
||||
${types.grid?.map(
|
||||
(source) => html`${source.flow_from.map((flow, idx) => {
|
||||
const entity = this.hass.states[flow.stat_energy_from];
|
||||
const energy =
|
||||
calculateStatisticSumGrowth(
|
||||
this._stats![flow.stat_energy_from]
|
||||
) || 0;
|
||||
totalGrid += energy;
|
||||
const cost_stat =
|
||||
flow.stat_cost ||
|
||||
this._energyInfo!.cost_sensors[flow.stat_energy_from];
|
||||
const cost = cost_stat
|
||||
? calculateStatisticSumGrowth(this._stats![cost_stat]) || 0
|
||||
: null;
|
||||
if (cost !== null) {
|
||||
totalCost += cost;
|
||||
}
|
||||
const color =
|
||||
idx > 0
|
||||
? rgb2hex(
|
||||
lab2rgb(
|
||||
labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
|
||||
)
|
||||
)
|
||||
: consumptionColor;
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
})}
|
||||
></div>
|
||||
</td>
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
${entity
|
||||
? computeStateName(entity)
|
||||
: flow.stat_energy_from}
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(energy, this.hass.locale)} kWh
|
||||
</td>
|
||||
${showCosts
|
||||
? html` <td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${cost !== null
|
||||
? formatNumber(cost, this.hass.locale, {
|
||||
style: "currency",
|
||||
currency: this.hass.config.currency!,
|
||||
})
|
||||
: ""}
|
||||
</td>`
|
||||
: ""}
|
||||
</tr>`;
|
||||
})}
|
||||
${source.flow_to.map((flow, idx) => {
|
||||
const entity = this.hass.states[flow.stat_energy_to];
|
||||
const energy =
|
||||
(calculateStatisticSumGrowth(
|
||||
this._stats![flow.stat_energy_to]
|
||||
) || 0) * -1;
|
||||
totalGrid += energy;
|
||||
const cost_stat =
|
||||
flow.stat_compensation ||
|
||||
this._energyInfo!.cost_sensors[flow.stat_energy_to];
|
||||
const cost = cost_stat
|
||||
? (calculateStatisticSumGrowth(this._stats![cost_stat]) ||
|
||||
0) * -1
|
||||
: null;
|
||||
if (cost !== null) {
|
||||
totalCost += cost;
|
||||
}
|
||||
const color =
|
||||
idx > 0
|
||||
? rgb2hex(
|
||||
lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx))
|
||||
)
|
||||
: returnColor;
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
borderColor: color,
|
||||
backgroundColor: color + "7F",
|
||||
})}
|
||||
></div>
|
||||
</td>
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
${entity ? computeStateName(entity) : flow.stat_energy_to}
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(energy, this.hass.locale)} kWh
|
||||
</td>
|
||||
${showCosts
|
||||
? html` <td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${cost !== null
|
||||
? formatNumber(cost, this.hass.locale, {
|
||||
style: "currency",
|
||||
currency: this.hass.config.currency!,
|
||||
})
|
||||
: ""}
|
||||
</td>`
|
||||
: ""}
|
||||
</tr>`;
|
||||
})}`
|
||||
)}
|
||||
${types.grid
|
||||
? html` <tr class="mdc-data-table__row total">
|
||||
<td class="mdc-data-table__cell"></td>
|
||||
<th class="mdc-data-table__cell" scope="row">Grid total</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(totalGrid, this.hass.locale)} kWh
|
||||
</td>
|
||||
${showCosts
|
||||
? html`<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${formatNumber(totalCost, this.hass.locale, {
|
||||
style: "currency",
|
||||
currency: this.hass.config.currency!,
|
||||
})}
|
||||
</td>`
|
||||
: ""}
|
||||
</tr>`
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private async _getEnergyInfo() {
|
||||
this._energyInfo = await getEnergyInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = Object.values(this._energyInfo!.cost_sensors);
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
statistics.push(source.stat_energy_from);
|
||||
} else {
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statistics.push(flowFrom.stat_cost);
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
if (flowTo.stat_compensation) {
|
||||
statistics.push(flowTo.stat_compensation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(dataTableStyles)}
|
||||
.mdc-data-table {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.mdc-data-table__header-cell,
|
||||
.mdc-data-table__cell {
|
||||
color: var(--primary-text-color);
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
.total {
|
||||
--mdc-typography-body2-font-weight: 500;
|
||||
}
|
||||
.total .mdc-data-table__cell {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.cell-bullet {
|
||||
width: 32px;
|
||||
padding-right: 0;
|
||||
}
|
||||
.bullet {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 4px;
|
||||
height: 16px;
|
||||
width: 32px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-sources-table-card": HuiEnergySourcesTableCard;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@ -8,47 +9,40 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../components/ha-card";
|
||||
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergySummaryGraphCardConfig } from "./types";
|
||||
import { fetchStatistics, Statistics } from "../../../data/history";
|
||||
import {
|
||||
hex2rgb,
|
||||
lab2rgb,
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
} from "../../../common/color/convert-color";
|
||||
import { labDarken } from "../../../common/color/lab";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import { round } from "../../../common/number/round";
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { hexBlend } from "../../../../common/color/hex";
|
||||
import { labDarken } from "../../../../common/color/lab";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/string/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { fetchStatistics, Statistics } from "../../../../data/history";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyUsageGraphCardConfig } from "../types";
|
||||
|
||||
const NEGATIVE = ["to_grid"];
|
||||
const ORDER = {
|
||||
used_solar: 0,
|
||||
from_grid: 100,
|
||||
to_grid: 200,
|
||||
};
|
||||
const COLORS = {
|
||||
to_grid: { border: "#56d256", background: "#87ceab" },
|
||||
from_grid: { border: "#126A9A", background: "#88b5cd" },
|
||||
used_solar: { border: "#FF9800", background: "#ffcb80" },
|
||||
};
|
||||
|
||||
@customElement("hui-energy-summary-graph-card")
|
||||
export class HuiEnergySummaryGraphCard
|
||||
@customElement("hui-energy-usage-graph-card")
|
||||
export class HuiEnergyUsageGraphCard
|
||||
extends LitElement
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergySummaryGraphCardConfig;
|
||||
@state() private _config?: EnergyUsageGraphCardConfig;
|
||||
|
||||
@state() private _data?: Statistics;
|
||||
|
||||
@state() private _chartData?: ChartData;
|
||||
@state() private _chartData: ChartData = {
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
@ -82,7 +76,7 @@ export class HuiEnergySummaryGraphCard
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergySummaryGraphCardConfig): void {
|
||||
public setConfig(config: EnergyUsageGraphCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@ -96,7 +90,7 @@ export class HuiEnergySummaryGraphCard
|
||||
}
|
||||
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| EnergySummaryGraphCardConfig
|
||||
| EnergyUsageGraphCardConfig
|
||||
| undefined;
|
||||
|
||||
if (oldConfig !== this._config) {
|
||||
@ -116,31 +110,38 @@ export class HuiEnergySummaryGraphCard
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header="${this._config.title}">
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
})}"
|
||||
>
|
||||
${this._chartData
|
||||
? html`<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
chartType="line"
|
||||
></ha-chart-base>`
|
||||
: ""}
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.options=${this._chartOptions}
|
||||
chart-type="bar"
|
||||
></ha-chart-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createOptions() {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
const startTime = startDate.getTime();
|
||||
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: startTime,
|
||||
suggestedMax: startTime + 24 * 60 * 60 * 1000,
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
@ -159,15 +160,21 @@ export class HuiEnergySummaryGraphCard
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat: "datetimeseconds",
|
||||
tooltipFormat: "datetime",
|
||||
},
|
||||
offset: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: "kWh",
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: (value) => Math.abs(round(value)),
|
||||
callback: (value) =>
|
||||
formatNumber(Math.abs(value), this.hass.locale),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -179,7 +186,10 @@ export class HuiEnergySummaryGraphCard
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${Math.abs(context.parsed.y)} kWh`,
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
Math.abs(context.parsed.y),
|
||||
this.hass.locale
|
||||
)} kWh`,
|
||||
footer: (contexts) => {
|
||||
let totalConsumed = 0;
|
||||
let totalReturned = 0;
|
||||
@ -193,9 +203,19 @@ export class HuiEnergySummaryGraphCard
|
||||
}
|
||||
}
|
||||
return [
|
||||
`Total consumed: ${totalConsumed.toFixed(2)} kWh`,
|
||||
`Total returned: ${totalReturned.toFixed(2)} kWh`,
|
||||
];
|
||||
totalConsumed
|
||||
? `Total consumed: ${formatNumber(
|
||||
totalConsumed,
|
||||
this.hass.locale
|
||||
)} kWh`
|
||||
: "",
|
||||
totalReturned
|
||||
? `Total returned: ${formatNumber(
|
||||
totalReturned,
|
||||
this.hass.locale
|
||||
)} kWh`
|
||||
: "",
|
||||
].filter(Boolean);
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -213,14 +233,13 @@ export class HuiEnergySummaryGraphCard
|
||||
mode: "nearest",
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 5,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
|
||||
@ -280,7 +299,7 @@ export class HuiEnergySummaryGraphCard
|
||||
}
|
||||
|
||||
const statisticsData = Object.values(this._data!);
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
const datasets: ChartDataset<"bar">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (statisticsData.length === 0) {
|
||||
@ -304,6 +323,23 @@ export class HuiEnergySummaryGraphCard
|
||||
} = {};
|
||||
const summedData: { [key: string]: { [start: string]: number } } = {};
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const colors = {
|
||||
to_grid: computedStyles
|
||||
.getPropertyValue("--energy-grid-return-color")
|
||||
.trim(),
|
||||
from_grid: computedStyles
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim(),
|
||||
used_solar: computedStyles
|
||||
.getPropertyValue("--energy-solar-color")
|
||||
.trim(),
|
||||
};
|
||||
|
||||
const backgroundColor = computedStyles
|
||||
.getPropertyValue("--card-background-color")
|
||||
.trim();
|
||||
|
||||
Object.entries(statistics).forEach(([key, statIds]) => {
|
||||
const sum = ["solar", "to_grid"].includes(key);
|
||||
const add = key !== "solar";
|
||||
@ -330,7 +366,7 @@ export class HuiEnergySummaryGraphCard
|
||||
totalStats[stat.start] =
|
||||
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||
}
|
||||
if (add) {
|
||||
if (add && !(stat.start in set)) {
|
||||
set[stat.start] = val;
|
||||
}
|
||||
prevValue = stat.sum;
|
||||
@ -367,12 +403,13 @@ export class HuiEnergySummaryGraphCard
|
||||
const uniqueKeys = Array.from(new Set(allKeys));
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
const negative = NEGATIVE.includes(type);
|
||||
|
||||
Object.entries(sources).forEach(([statId, source], idx) => {
|
||||
const data: ChartDataset<"line">[] = [];
|
||||
const data: ChartDataset<"bar">[] = [];
|
||||
const entity = this.hass.states[statId];
|
||||
const color = COLORS[type];
|
||||
const borderColor =
|
||||
idx > 0
|
||||
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx)))
|
||||
: colors[type];
|
||||
|
||||
data.push({
|
||||
label:
|
||||
@ -381,30 +418,20 @@ export class HuiEnergySummaryGraphCard
|
||||
: entity
|
||||
? computeStateName(entity)
|
||||
: statId,
|
||||
fill: true,
|
||||
stepped: false,
|
||||
order: ORDER[type] + idx,
|
||||
borderColor:
|
||||
idx > 0
|
||||
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(color.border)), idx)))
|
||||
: color.border,
|
||||
backgroundColor:
|
||||
idx > 0
|
||||
? rgb2hex(
|
||||
lab2rgb(labDarken(rgb2lab(hex2rgb(color.background)), idx))
|
||||
)
|
||||
: color.background,
|
||||
stack: negative ? "negative" : "positive",
|
||||
borderColor,
|
||||
backgroundColor: hexBlend(borderColor, backgroundColor, 50),
|
||||
stack: "stack",
|
||||
data: [],
|
||||
});
|
||||
|
||||
// Process chart data.
|
||||
for (const key of uniqueKeys) {
|
||||
const value = key in source ? Math.round(source[key] * 100) / 100 : 0;
|
||||
const value = source[key] || 0;
|
||||
const date = new Date(key);
|
||||
// @ts-expect-error
|
||||
data[0].data.push({
|
||||
x: date.getTime(),
|
||||
y: value && negative ? -1 * value : value,
|
||||
y: value && type === "to_grid" ? -1 * value : value,
|
||||
});
|
||||
}
|
||||
|
||||
@ -423,6 +450,9 @@ export class HuiEnergySummaryGraphCard
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
@ -435,6 +465,6 @@ export class HuiEnergySummaryGraphCard
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-summary-graph-card": HuiEnergySummaryGraphCard;
|
||||
"hui-energy-usage-graph-card": HuiEnergyUsageGraphCard;
|
||||
}
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
// @ts-ignore
|
||||
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { round } from "../../../common/number/round";
|
||||
import "../../../components/chart/statistics-chart";
|
||||
import "../../../components/ha-card";
|
||||
import {
|
||||
EnergyInfo,
|
||||
getEnergyInfo,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergyDevicesGraphCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-energy-costs-table-card")
|
||||
export class HuiEnergyCostsTableCard
|
||||
extends LitElement
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergyDevicesGraphCardConfig;
|
||||
|
||||
@state() private _stats?: Statistics;
|
||||
|
||||
@state() private _energyInfo?: EnergyInfo;
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergyDevicesGraphCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this._getEnergyInfo().then(() => this._getStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats) {
|
||||
return html`Loading...`;
|
||||
}
|
||||
|
||||
const source = this._config.prefs.energy_sources?.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
if (!source) {
|
||||
return html`No grid source found.`;
|
||||
}
|
||||
|
||||
let totalEnergy = 0;
|
||||
let totalCost = 0;
|
||||
|
||||
return html` <ha-card .header="${this._config.title}">
|
||||
<div class="mdc-data-table">
|
||||
<div class="mdc-data-table__table-container">
|
||||
<table class="mdc-data-table__table" aria-label="Dessert calories">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
<th
|
||||
class="mdc-data-table__header-cell"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Grid source
|
||||
</th>
|
||||
<th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Energy
|
||||
</th>
|
||||
<th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
Cost
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="mdc-data-table__content">
|
||||
${source.flow_from.map((flow) => {
|
||||
const entity = this.hass.states[flow.stat_energy_from];
|
||||
const energy =
|
||||
calculateStatisticSumGrowth(
|
||||
this._stats![flow.stat_energy_from]
|
||||
) || 0;
|
||||
totalEnergy += energy;
|
||||
const cost_stat =
|
||||
flow.stat_cost ||
|
||||
this._energyInfo!.cost_sensors[flow.stat_energy_from];
|
||||
const cost =
|
||||
(cost_stat &&
|
||||
calculateStatisticSumGrowth(this._stats![cost_stat])) ||
|
||||
0;
|
||||
totalCost += cost;
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
${entity ? computeStateName(entity) : flow.stat_energy_from}
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${round(energy)} kWh
|
||||
</td>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${this._config!.prefs.currency} ${cost.toFixed(2)}
|
||||
</td>
|
||||
</tr>`;
|
||||
})}
|
||||
${source.flow_to.map((flow) => {
|
||||
const entity = this.hass.states[flow.stat_energy_to];
|
||||
const energy =
|
||||
(calculateStatisticSumGrowth(
|
||||
this._stats![flow.stat_energy_to]
|
||||
) || 0) * -1;
|
||||
totalEnergy += energy;
|
||||
const cost_stat =
|
||||
flow.stat_compensation ||
|
||||
this._energyInfo!.cost_sensors[flow.stat_energy_to];
|
||||
const cost =
|
||||
((cost_stat &&
|
||||
calculateStatisticSumGrowth(this._stats![cost_stat])) ||
|
||||
0) * -1;
|
||||
totalCost += cost;
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
${entity ? computeStateName(entity) : flow.stat_energy_to}
|
||||
</th>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${round(energy)} kWh
|
||||
</td>
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
${this._config!.prefs.currency} ${cost.toFixed(2)}
|
||||
</td>
|
||||
</tr>`;
|
||||
})}
|
||||
<tr class="mdc-data-table__row total">
|
||||
<th class="mdc-data-table__cell" scope="row">Total</th>
|
||||
<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
|
||||
${round(totalEnergy)} kWh
|
||||
</td>
|
||||
<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
|
||||
${this._config!.prefs.currency} ${totalCost.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private async _getEnergyInfo() {
|
||||
this._energyInfo = await getEnergyInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = Object.values(this._energyInfo!.cost_sensors);
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statistics.push(flowFrom.stat_cost);
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
if (flowTo.stat_compensation) {
|
||||
statistics.push(flowTo.stat_compensation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(dataTableStyles)}
|
||||
.mdc-data-table {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.total {
|
||||
background-color: var(--primary-background-color);
|
||||
--mdc-typography-body2-font-weight: 500;
|
||||
}
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-costs-table-card": HuiEnergyCostsTableCard;
|
||||
}
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
import { mdiCashMultiple, mdiSolarPower } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
energySourcesByType,
|
||||
GridSourceTypeEnergyPreference,
|
||||
SolarSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergySummaryCardConfig } from "./types";
|
||||
import "../../../components/ha-card";
|
||||
|
||||
const renderSumStatHelper = (
|
||||
data: Statistics,
|
||||
stats: string[],
|
||||
unit: string
|
||||
) => {
|
||||
let totalGrowth = 0;
|
||||
|
||||
for (const stat of stats) {
|
||||
if (!(stat in data)) {
|
||||
return "stat missing";
|
||||
}
|
||||
const statGrowth = calculateStatisticSumGrowth(data[stat]);
|
||||
|
||||
if (statGrowth === null) {
|
||||
return "incomplete data";
|
||||
}
|
||||
|
||||
totalGrowth += statGrowth;
|
||||
}
|
||||
|
||||
return `${totalGrowth.toFixed(2)} ${unit}`;
|
||||
};
|
||||
|
||||
@customElement("hui-energy-summary-card")
|
||||
class HuiEnergySummaryCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergySummaryCardConfig;
|
||||
|
||||
@state() private _data?: Statistics;
|
||||
|
||||
private _fetching = false;
|
||||
|
||||
public setConfig(config: EnergySummaryCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._fetching && !this._data) {
|
||||
this._getStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
const hasConsumption = types.grid !== undefined;
|
||||
const hasProduction = types.solar !== undefined;
|
||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||
const hasCost =
|
||||
hasConsumption &&
|
||||
types.grid![0].flow_from.some((flow) => flow.stat_cost !== null);
|
||||
|
||||
// total consumption = consumption_from_grid + solar_production - return_to_grid
|
||||
|
||||
return html`
|
||||
<ha-card header="Today">
|
||||
<div class="card-content">
|
||||
${!hasConsumption
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiCashMultiple}></ha-svg-icon>
|
||||
<div class="label">Total Consumption</div>
|
||||
<div class="data">
|
||||
${!this._data
|
||||
? ""
|
||||
: renderSumStatHelper(
|
||||
this._data,
|
||||
types.grid![0].flow_from.map(
|
||||
(flow) => flow.stat_energy_from
|
||||
),
|
||||
"kWh"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${!hasProduction
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiSolarPower}></ha-svg-icon>
|
||||
<div class="label">Total Production</div>
|
||||
<div class="data">
|
||||
${!this._data
|
||||
? ""
|
||||
: renderSumStatHelper(
|
||||
this._data,
|
||||
types.solar!.map((source) => source.stat_energy_from),
|
||||
"kWh"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${!hasReturnToGrid
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiCashMultiple}></ha-svg-icon>
|
||||
<div class="label">Production returned to grid</div>
|
||||
<div class="data">
|
||||
${!this._data
|
||||
? ""
|
||||
: renderSumStatHelper(
|
||||
this._data,
|
||||
types.grid![0].flow_to.map(
|
||||
(flow) => flow.stat_energy_to
|
||||
),
|
||||
"kWh"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${!hasReturnToGrid || !hasProduction
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiCashMultiple}></ha-svg-icon>
|
||||
<div class="label">Amount of produced power self used</div>
|
||||
<div class="data">
|
||||
${!this._data
|
||||
? ""
|
||||
: this._renderSolarPowerConsumptionRatio(
|
||||
types.solar![0],
|
||||
types.grid![0]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${!hasCost
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiCashMultiple}></ha-svg-icon>
|
||||
<div class="label">Total costs of today</div>
|
||||
<div class="data">
|
||||
${!this._data
|
||||
? ""
|
||||
: renderSumStatHelper(
|
||||
this._data,
|
||||
types
|
||||
.grid![0].flow_from.map((flow) => flow.stat_cost)
|
||||
.filter(Boolean) as string[],
|
||||
prefs.currency
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
// This is superduper temp.
|
||||
private async _getStatistics(): Promise<void> {
|
||||
if (this._fetching) {
|
||||
return;
|
||||
}
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
this._fetching = true;
|
||||
const statistics: string[] = [];
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
statistics.push(source.stat_energy_from);
|
||||
// Use ws command to get solar forecast
|
||||
|
||||
// if (source.stat_predicted_energy_from) {
|
||||
// statistics.push(source.stat_predicted_energy_from);
|
||||
// }
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statistics.push(flowFrom.stat_cost);
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this._data = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
} finally {
|
||||
this._fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderSolarPowerConsumptionRatio(
|
||||
solarSource: SolarSourceTypeEnergyPreference,
|
||||
gridSource: GridSourceTypeEnergyPreference
|
||||
) {
|
||||
let returnToGrid = 0;
|
||||
|
||||
for (const flowTo of gridSource.flow_to) {
|
||||
if (!flowTo.stat_energy_to || !(flowTo.stat_energy_to in this._data!)) {
|
||||
continue;
|
||||
}
|
||||
const flowReturned = calculateStatisticSumGrowth(
|
||||
this._data![flowTo.stat_energy_to]
|
||||
);
|
||||
if (flowReturned === null) {
|
||||
return "incomplete return data";
|
||||
}
|
||||
returnToGrid += flowReturned;
|
||||
}
|
||||
|
||||
if (!(solarSource.stat_energy_from in this._data!)) {
|
||||
return "sun stat missing";
|
||||
}
|
||||
|
||||
const production = calculateStatisticSumGrowth(
|
||||
this._data![solarSource.stat_energy_from]
|
||||
);
|
||||
|
||||
if (production === null) {
|
||||
return "incomplete solar data";
|
||||
}
|
||||
|
||||
if (production === 0) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const consumed = Math.max(
|
||||
Math.min(((production - returnToGrid) / production) * 100, 100),
|
||||
0
|
||||
);
|
||||
|
||||
return `${consumed.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding: 8px;
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.label {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.data {
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-summary-card": HuiEnergySummaryCard;
|
||||
}
|
||||
}
|
@ -1,333 +0,0 @@
|
||||
import { mdiHome, mdiLeaf, mdiSolarPower, mdiTransmissionTower } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import { energySourcesByType } from "../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EnergySummaryCardConfig } from "./types";
|
||||
import "../../../components/ha-card";
|
||||
import { round } from "../../../common/number/round";
|
||||
|
||||
@customElement("hui-energy-usage-card")
|
||||
class HuiEnergyUsageCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergySummaryCardConfig;
|
||||
|
||||
@state() private _stats?: Statistics;
|
||||
|
||||
@state() private _co2SignalEntity?: string;
|
||||
|
||||
private _fetching = false;
|
||||
|
||||
public setConfig(config: EnergySummaryCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._fetching && !this._stats) {
|
||||
this._fetching = true;
|
||||
Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then(
|
||||
() => {
|
||||
this._fetching = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats) {
|
||||
return html`Loading…`;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
// The strategy only includes this card if we have a grid.
|
||||
const hasConsumption = true;
|
||||
|
||||
const hasSolarProduction = types.solar !== undefined;
|
||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||
|
||||
const totalGridConsumption = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
|
||||
);
|
||||
|
||||
if (totalGridConsumption === null) {
|
||||
return html`Total consumption couldn't be calculated`;
|
||||
}
|
||||
|
||||
let totalSolarProduction: number | null = null;
|
||||
|
||||
if (hasSolarProduction) {
|
||||
totalSolarProduction = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.solar!.map((source) => source.stat_energy_from)
|
||||
);
|
||||
|
||||
if (totalSolarProduction === null) {
|
||||
return html`Total production couldn't be calculated`;
|
||||
}
|
||||
}
|
||||
|
||||
let productionReturnedToGrid: number | null = null;
|
||||
|
||||
if (hasReturnToGrid) {
|
||||
productionReturnedToGrid = calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||
);
|
||||
|
||||
if (productionReturnedToGrid === undefined) {
|
||||
return html`Production returned to grid couldn't be calculated`;
|
||||
}
|
||||
}
|
||||
|
||||
// total consumption = consumption_from_grid + solar_production - return_to_grid
|
||||
|
||||
let co2percentage: number | undefined;
|
||||
|
||||
if (this._co2SignalEntity) {
|
||||
const co2State = this.hass.states[this._co2SignalEntity];
|
||||
if (co2State) {
|
||||
co2percentage = Number(co2State.state);
|
||||
if (isNaN(co2percentage)) {
|
||||
co2percentage = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We are calculating low carbon consumption based on what we got from the grid
|
||||
// minus what we gave back because what we gave back is low carbon
|
||||
const relativeGridFlow =
|
||||
totalGridConsumption - (productionReturnedToGrid || 0);
|
||||
|
||||
let lowCarbonConsumption: number | undefined;
|
||||
|
||||
if (co2percentage !== undefined) {
|
||||
if (relativeGridFlow > 0) {
|
||||
lowCarbonConsumption = round(relativeGridFlow * (co2percentage / 100));
|
||||
} else {
|
||||
lowCarbonConsumption = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const totalConsumption =
|
||||
totalGridConsumption +
|
||||
(totalSolarProduction || 0) -
|
||||
(productionReturnedToGrid || 0);
|
||||
|
||||
const gridPctLowCarbon =
|
||||
co2percentage === undefined ? 0 : co2percentage / 100;
|
||||
const gridPctHighCarbon = 1 - gridPctLowCarbon;
|
||||
|
||||
const homePctSolar =
|
||||
((totalSolarProduction || 0) - (productionReturnedToGrid || 0)) /
|
||||
totalConsumption;
|
||||
// When we know the ratio solar-grid, we can adjust the low/high carbon
|
||||
// percentages to reflect that.
|
||||
const homePctGridLowCarbon = gridPctLowCarbon * (1 - homePctSolar);
|
||||
const homePctGridHighCarbon = gridPctHighCarbon * (1 - homePctSolar);
|
||||
|
||||
return html`
|
||||
<ha-card header="Usage">
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
${co2percentage === undefined
|
||||
? ""
|
||||
: html`
|
||||
<div class="circle-container">
|
||||
<span class="label">Low-carbon</span>
|
||||
<div class="circle low-carbon">
|
||||
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
|
||||
${co2percentage}% / ${round(lowCarbonConsumption!)} kWh
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
<div class="circle-container">
|
||||
<span class="label">Solar</span>
|
||||
<div class="circle solar">
|
||||
<ha-svg-icon .path="${mdiSolarPower}"></ha-svg-icon>
|
||||
${round(totalSolarProduction || 0)} kWh
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="circle-container">
|
||||
<div class="circle grid">
|
||||
<ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon>
|
||||
${round(totalGridConsumption - (productionReturnedToGrid || 0))}
|
||||
kWh
|
||||
<ul>
|
||||
<li>
|
||||
Grid high carbon: ${round(gridPctHighCarbon * 100, 1)}%
|
||||
</li>
|
||||
<li>Grid low carbon: ${round(gridPctLowCarbon * 100, 1)}%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="label">Grid</span>
|
||||
</div>
|
||||
<div class="circle-container home">
|
||||
<div class="circle home">
|
||||
<ha-svg-icon .path="${mdiHome}"></ha-svg-icon>
|
||||
${round(totalConsumption)} kWh
|
||||
<ul>
|
||||
<li>
|
||||
Grid high carbon: ${round(homePctGridHighCarbon * 100)}%
|
||||
</li>
|
||||
<li>
|
||||
Grid low carbon: ${round(homePctGridLowCarbon * 100)}%
|
||||
</li>
|
||||
<li>Solar: ${round(homePctSolar * 100)}%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="label">Home</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchCO2SignalEntity() {
|
||||
const [configEntries, entityRegistryEntries] = await Promise.all([
|
||||
getConfigEntries(this.hass),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry),
|
||||
]);
|
||||
|
||||
const co2ConfigEntry = configEntries.find(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
|
||||
if (!co2ConfigEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entityRegistryEntries) {
|
||||
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The integration offers 2 entities. We want the % one.
|
||||
const co2State = this.hass.states[entry.entity_id];
|
||||
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._co2SignalEntity = co2State.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = [];
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
statistics.push(source.stat_energy_from);
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statistics.push(flowFrom.stat_energy_from);
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statistics.push(flowTo.stat_energy_to);
|
||||
}
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
undefined,
|
||||
statistics
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--mdc-icon-size: 26px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.circle-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
}
|
||||
.circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.label {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.circle-container:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.circle ul {
|
||||
display: none;
|
||||
}
|
||||
.low-carbon {
|
||||
border-color: #0da035;
|
||||
}
|
||||
.low-carbon ha-svg-icon {
|
||||
color: #0da035;
|
||||
}
|
||||
.solar {
|
||||
border-color: #ff9800;
|
||||
}
|
||||
.grid {
|
||||
border-color: #134763;
|
||||
}
|
||||
.circle-container.home {
|
||||
margin-left: 120px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-energy-usage-card": HuiEnergyUsageCard;
|
||||
}
|
||||
}
|
@ -135,6 +135,8 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(entityState),
|
||||
})}
|
||||
.needle=${this._config!.needle}
|
||||
.levels=${this._config!.needle ? this._severityLevels() : undefined}
|
||||
></ha-gauge>
|
||||
<div class="name">
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
@ -200,6 +202,20 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
return severityMap.normal;
|
||||
}
|
||||
|
||||
private _severityLevels() {
|
||||
const sections = this._config!.severity;
|
||||
|
||||
if (!sections) {
|
||||
return [{ level: 0, stroke: severityMap.normal }];
|
||||
}
|
||||
|
||||
const sectionsArray = Object.keys(sections);
|
||||
return sectionsArray.map((severity) => ({
|
||||
level: sections[severity],
|
||||
stroke: severityMap[severity],
|
||||
}));
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
|
@ -133,6 +133,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._config,
|
||||
this._configEntities
|
||||
)}
|
||||
.zoom=${this._config.default_zoom ?? 14}
|
||||
.paths=${this._getHistoryPaths(this._config, this._history)}
|
||||
.darkMode=${this._config.dark_mode}
|
||||
></ha-map>
|
||||
|
@ -19,6 +19,15 @@ import { fetchStatistics, Statistics } from "../../../data/history";
|
||||
|
||||
@customElement("hui-statistics-graph-card")
|
||||
export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement() {
|
||||
await import("../editor/config-elements/hui-statistics-graph-card-editor");
|
||||
return document.createElement("hui-statistics-graph-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): StatisticsGraphCardConfig {
|
||||
return { type: "statistics-graph", entities: [] };
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _statistics?: Statistics;
|
||||
@ -105,7 +114,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
| StatisticsGraphCardConfig
|
||||
| undefined;
|
||||
|
||||
if (oldConfig?.entities !== this._config.entities) {
|
||||
if (
|
||||
oldConfig?.entities !== this._config.entities ||
|
||||
oldConfig?.days_to_show !== this._config.days_to_show
|
||||
) {
|
||||
this._getStatistics();
|
||||
// statistics are created every hour
|
||||
clearInterval(this._interval);
|
||||
@ -146,7 +158,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
const startDate = new Date();
|
||||
startDate.setHours(-24 * (this._config!.days_to_show || 30));
|
||||
startDate.setTime(
|
||||
startDate.getTime() -
|
||||
1000 * 60 * 60 * (24 * (this._config!.days_to_show || 30) + 1)
|
||||
);
|
||||
this._fetching = true;
|
||||
try {
|
||||
this._statistics = await fetchStatistics(
|
||||
|
@ -92,31 +92,54 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-summary";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergySummaryGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-distribution";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-summary-graph";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-solar-graph";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-devices-graph";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-sources-table";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-solar-consumed-gauge";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-grid-result-gauge";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-carbon-consumed-gauge";
|
||||
title?: string;
|
||||
prefs: EnergyPreferences;
|
||||
}
|
||||
|
||||
@ -147,6 +170,7 @@ export interface GaugeCardConfig extends LovelaceCardConfig {
|
||||
max?: number;
|
||||
severity?: SeverityConfig;
|
||||
theme?: string;
|
||||
needle?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigEntity extends EntityConfig {
|
||||
|
@ -35,18 +35,22 @@ const LAZY_LOAD_TYPES = {
|
||||
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"empty-state": () => import("../cards/hui-empty-state-card"),
|
||||
"energy-summary": () => import("../cards/hui-energy-summary-card"),
|
||||
"energy-summary-graph": () =>
|
||||
import("../cards/hui-energy-summary-graph-card"),
|
||||
"energy-solar-graph": () => import("../cards/hui-energy-solar-graph-card"),
|
||||
"energy-usage-graph": () =>
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-solar-graph": () =>
|
||||
import("../cards/energy/hui-energy-solar-graph-card"),
|
||||
"energy-devices-graph": () =>
|
||||
import("../cards/hui-energy-devices-graph-card"),
|
||||
"energy-costs-table": () => import("../cards/hui-energy-costs-table-card"),
|
||||
"energy-usage": () => import("../cards/hui-energy-usage-card"),
|
||||
import("../cards/energy/hui-energy-devices-graph-card"),
|
||||
"energy-sources-table": () =>
|
||||
import("../cards/energy/hui-energy-sources-table-card"),
|
||||
"energy-distribution": () =>
|
||||
import("../cards/energy/hui-energy-distribution-card"),
|
||||
"energy-solar-consumed-gauge": () =>
|
||||
import("../cards/hui-energy-solar-consumed-gauge-card"),
|
||||
import("../cards/energy/hui-energy-solar-consumed-gauge-card"),
|
||||
"energy-grid-neutrality-gauge": () =>
|
||||
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
|
||||
"energy-carbon-consumed-gauge": () =>
|
||||
import("../cards/hui-energy-carbon-consumed-gauge-card"),
|
||||
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
|
||||
grid: () => import("../cards/hui-grid-card"),
|
||||
starting: () => import("../cards/hui-starting-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, number, object, optional, string } from "superstruct";
|
||||
import { assert, boolean, number, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/ha-formfield";
|
||||
@ -23,6 +23,7 @@ const cardConfigStruct = object({
|
||||
max: optional(number()),
|
||||
severity: optional(object()),
|
||||
theme: optional(string()),
|
||||
needle: optional(boolean()),
|
||||
});
|
||||
|
||||
const includeDomains = ["counter", "input_number", "number", "sensor"];
|
||||
@ -137,6 +138,17 @@ export class HuiGaugeCardEditor
|
||||
.configValue=${"max"}
|
||||
@value-changed="${this._valueChanged}"
|
||||
></paper-input>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.gauge.needle_gauge"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked="${this._config!.needle !== undefined}"
|
||||
@change="${this._toggleNeedle}"
|
||||
></ha-switch
|
||||
></ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.gauge.severity.define"
|
||||
@ -212,6 +224,22 @@ export class HuiGaugeCardEditor
|
||||
];
|
||||
}
|
||||
|
||||
private _toggleNeedle(ev: EntitiesEditorEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
if ((ev.target as EditorTarget).checked) {
|
||||
this._config = {
|
||||
...this._config,
|
||||
needle: true,
|
||||
};
|
||||
} else {
|
||||
this._config = { ...this._config };
|
||||
delete this._config.needle;
|
||||
}
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _toggleSeverity(ev: EntitiesEditorEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
|
@ -38,10 +38,6 @@ export class HuiHistoryGraphCardEditor
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
}
|
||||
|
||||
get _entity(): string {
|
||||
return this._config!.entity || "";
|
||||
}
|
||||
|
||||
get _title(): string {
|
||||
return this._config!.title || "";
|
||||
}
|
||||
|
@ -0,0 +1,238 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
literal,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { StatisticsGraphCardConfig } from "../../cards/types";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import { EditorTarget } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "../../../../components/entity/ha-statistics-picker";
|
||||
import { processConfigEntities } from "../../common/process-config-entities";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import { StatisticType } from "../../../../data/history";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
|
||||
const statTypeStruct = union([
|
||||
literal("sum"),
|
||||
literal("min"),
|
||||
literal("max"),
|
||||
literal("mean"),
|
||||
]);
|
||||
|
||||
const cardConfigStruct = object({
|
||||
type: string(),
|
||||
entities: array(entitiesConfigStruct),
|
||||
title: optional(string()),
|
||||
days_to_show: optional(number()),
|
||||
chart_type: optional(union([literal("bar"), literal("line")])),
|
||||
stat_types: optional(union([array(statTypeStruct), statTypeStruct])),
|
||||
});
|
||||
|
||||
@customElement("hui-statistics-graph-card-editor")
|
||||
export class HuiStatisticsGraphCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: StatisticsGraphCardConfig;
|
||||
|
||||
@state() private _configEntities?: string[];
|
||||
|
||||
public setConfig(config: StatisticsGraphCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = config.entities
|
||||
? processConfigEntities(config.entities).map((cfg) => cfg.entity)
|
||||
: [];
|
||||
}
|
||||
|
||||
get _title(): string {
|
||||
return this._config!.title || "";
|
||||
}
|
||||
|
||||
get _days_to_show(): number {
|
||||
return this._config!.days_to_show || 30;
|
||||
}
|
||||
|
||||
get _chart_type(): StatisticsGraphCardConfig["chart_type"] {
|
||||
return this._config!.chart_type || "line";
|
||||
}
|
||||
|
||||
get _stat_types(): StatisticType[] {
|
||||
return this._config!.stat_types
|
||||
? Array.isArray(this._config!.stat_types)
|
||||
? this._config!.stat_types
|
||||
: [this._config!.stat_types]
|
||||
: ["mean", "min", "max", "sum"];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="card-config">
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.title"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._title}
|
||||
.configValue=${"title"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
type="number"
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.days_to_show"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._days_to_show}
|
||||
min="1"
|
||||
.configValue=${"days_to_show"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<p>Show stat types:</p>
|
||||
<div class="side-by-side">
|
||||
<ha-formfield label="Sum">
|
||||
<ha-checkbox
|
||||
.checked=${this._stat_types.includes("sum")}
|
||||
name="sum"
|
||||
@change=${this._statTypesChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Mean">
|
||||
<ha-checkbox
|
||||
.checked=${this._stat_types.includes("mean")}
|
||||
name="mean"
|
||||
@change=${this._statTypesChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Min">
|
||||
<ha-checkbox
|
||||
.checked=${this._stat_types.includes("min")}
|
||||
name="min"
|
||||
@change=${this._statTypesChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Max">
|
||||
<ha-checkbox
|
||||
.checked=${this._stat_types.includes("max")}
|
||||
name="max"
|
||||
@change=${this._statTypesChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div class="side-by-side">
|
||||
<p>Chart type:</p>
|
||||
<ha-formfield label="Line">
|
||||
<ha-radio
|
||||
.checked=${this._chart_type === "line"}
|
||||
value="line"
|
||||
name="chart_type"
|
||||
@change=${this._chartTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Bar">
|
||||
<ha-radio
|
||||
.checked=${this._chart_type === "bar"}
|
||||
value="bar"
|
||||
name="chart_type"
|
||||
@change=${this._chartTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<ha-statistics-picker
|
||||
.hass=${this.hass}
|
||||
.pickStatisticLabel=${`Add a statistic`}
|
||||
.pickedStatisticLabel=${`Statistic`}
|
||||
.value=${this._configEntities}
|
||||
.configValue=${"entities"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-statistics-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _chartTypeChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...this._config!, chart_type: input.value },
|
||||
});
|
||||
}
|
||||
|
||||
private _statTypesChanged(ev) {
|
||||
const name = ev.currentTarget.name;
|
||||
const checked = ev.currentTarget.checked;
|
||||
if (checked) {
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...this._config!, stat_types: [...this._stat_types, name] },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const statTypes = [...this._stat_types];
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config!,
|
||||
stat_types: statTypes.filter((stat) => stat !== name),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const target = ev.target! as EditorTarget;
|
||||
|
||||
const newValue = ev.detail?.value || target.value;
|
||||
|
||||
if (this[`_${target.configValue}`] === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === "") {
|
||||
this._config = { ...this._config };
|
||||
delete this._config[target.configValue!];
|
||||
} else {
|
||||
let value: any = newValue;
|
||||
if (target.type === "number") {
|
||||
value = Number(value);
|
||||
}
|
||||
this._config = {
|
||||
...this._config,
|
||||
[target.configValue!]: value,
|
||||
};
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return configElementStyle;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-statistics-graph-card-editor": HuiStatisticsGraphCardEditor;
|
||||
}
|
||||
}
|
@ -33,6 +33,10 @@ export const coreCards: Card[] = [
|
||||
type: "history-graph",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "statistics-graph",
|
||||
showElement: false,
|
||||
},
|
||||
{
|
||||
type: "humidifier",
|
||||
showElement: true,
|
||||
|
@ -64,6 +64,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
private _mqls?: MediaQueryList[];
|
||||
|
||||
private _mqlListenerRef?: () => void;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this.addEventListener("iron-resize", (ev: Event) => ev.stopPropagation());
|
||||
@ -77,8 +79,9 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._mqls?.forEach((mql) => {
|
||||
mql.removeListener(this._updateColumns);
|
||||
mql.removeListener(this._mqlListenerRef!);
|
||||
});
|
||||
this._mqlListenerRef = undefined;
|
||||
this._mqls = undefined;
|
||||
}
|
||||
|
||||
@ -112,7 +115,10 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
private _initMqls() {
|
||||
this._mqls = [300, 600, 900, 1200].map((width) => {
|
||||
const mql = window.matchMedia(`(min-width: ${width}px)`);
|
||||
mql.addListener(this._updateColumns.bind(this));
|
||||
if (!this._mqlListenerRef) {
|
||||
this._mqlListenerRef = this._updateColumns.bind(this);
|
||||
}
|
||||
mql.addListener(this._mqlListenerRef);
|
||||
return mql;
|
||||
});
|
||||
}
|
||||
|
@ -17,10 +17,9 @@ import type {
|
||||
} from "../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import { HuiCardOptions } from "../components/hui-card-options";
|
||||
import type { Lovelace, LovelaceCard } from "../types";
|
||||
|
||||
let editCodeLoaded = false;
|
||||
|
||||
export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@ -36,6 +35,24 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@state() private _config?: LovelaceViewConfig;
|
||||
|
||||
private _mqlListenerRef?: () => void;
|
||||
|
||||
private _mql?: MediaQueryList;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._mql = window.matchMedia("(min-width: 760px)");
|
||||
this._mqlListenerRef = this._createCards.bind(this);
|
||||
this._mql.addListener(this._mqlListenerRef);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._mql?.removeListener(this._mqlListenerRef!);
|
||||
this._mqlListenerRef = undefined;
|
||||
this._mql = undefined;
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceViewConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
@ -43,8 +60,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
public willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (this.lovelace?.editMode && !editCodeLoaded) {
|
||||
editCodeLoaded = true;
|
||||
if (this.lovelace?.editMode) {
|
||||
import("./default-view-editable");
|
||||
}
|
||||
|
||||
@ -71,7 +87,8 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.lovelace?.editMode && this.cards.length === 0
|
||||
<div class="container"></div>
|
||||
${this.lovelace?.editMode
|
||||
? html`
|
||||
<ha-fab
|
||||
.label=${this.hass!.localize(
|
||||
@ -97,49 +114,53 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
private _createCards(): void {
|
||||
const mainDiv = document.createElement("div");
|
||||
mainDiv.id = "main";
|
||||
const sidebarDiv = document.createElement("div");
|
||||
sidebarDiv.id = "sidebar";
|
||||
|
||||
let sidebarDiv: HTMLDivElement;
|
||||
if (this._mql?.matches) {
|
||||
sidebarDiv = document.createElement("div");
|
||||
sidebarDiv.id = "sidebar";
|
||||
} else {
|
||||
sidebarDiv = mainDiv;
|
||||
}
|
||||
|
||||
if (this.hasUpdated) {
|
||||
const oldMain = this.renderRoot.querySelector("#main");
|
||||
const oldSidebar = this.renderRoot.querySelector("#sidebar");
|
||||
const container = this.renderRoot.querySelector(".container")!;
|
||||
if (oldMain) {
|
||||
this.renderRoot.removeChild(oldMain);
|
||||
container.removeChild(oldMain);
|
||||
}
|
||||
if (oldSidebar) {
|
||||
this.renderRoot.removeChild(oldSidebar);
|
||||
container.removeChild(oldSidebar);
|
||||
}
|
||||
this.renderRoot.appendChild(mainDiv);
|
||||
this.renderRoot.appendChild(sidebarDiv);
|
||||
container.appendChild(mainDiv);
|
||||
container.appendChild(sidebarDiv);
|
||||
} else {
|
||||
this.updateComplete.then(() => {
|
||||
this.renderRoot.appendChild(mainDiv);
|
||||
this.renderRoot.appendChild(sidebarDiv);
|
||||
const container = this.renderRoot.querySelector(".container")!;
|
||||
container.appendChild(mainDiv);
|
||||
container.appendChild(sidebarDiv);
|
||||
});
|
||||
}
|
||||
|
||||
this.cards.forEach((card: LovelaceCard, idx) => {
|
||||
const cardConfig = this._config?.cards?.[idx];
|
||||
let element: LovelaceCard | HuiCardOptions;
|
||||
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||
card.editMode = false;
|
||||
if (cardConfig?.view_layout?.position !== "sidebar") {
|
||||
mainDiv.appendChild(card);
|
||||
} else {
|
||||
sidebarDiv.appendChild(card);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.lovelace = this.lovelace;
|
||||
wrapper.path = [this.index!, 0];
|
||||
card.editMode = true;
|
||||
wrapper.appendChild(card);
|
||||
if (cardConfig?.view_layout?.position !== "sidebar") {
|
||||
mainDiv.appendChild(card);
|
||||
element = card;
|
||||
} else {
|
||||
sidebarDiv.appendChild(card);
|
||||
element = document.createElement("hui-card-options");
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.path = [this.index!, idx];
|
||||
card.editMode = true;
|
||||
element.appendChild(card);
|
||||
}
|
||||
if (cardConfig?.view_layout?.position !== "sidebar") {
|
||||
mainDiv.appendChild(element);
|
||||
} else {
|
||||
sidebarDiv.appendChild(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -147,13 +168,17 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#main {
|
||||
@ -166,27 +191,18 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
:host > div {
|
||||
.container > div {
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host > div > * {
|
||||
.container > div > * {
|
||||
display: block;
|
||||
margin: var(--masonry-view-card-margin, 4px 4px 8px);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
:host {
|
||||
flex-direction: column;
|
||||
}
|
||||
#sidebar {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host > div > * {
|
||||
.container > div > * {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -82,6 +82,14 @@ documentContainer.innerHTML = `<custom-style>
|
||||
--state-climate-dry-color: #efbd07;
|
||||
--state-climate-idle-color: #8a8a8a;
|
||||
|
||||
/* energy */
|
||||
--energy-grid-consumption-color: #126a9a;
|
||||
--energy-grid-return-color: #673ab7;
|
||||
--energy-solar-color: #ff9800;
|
||||
--energy-non-fossil-color: #0f9d58;
|
||||
|
||||
--rgb-energy-solar-color: 255, 152, 0;
|
||||
|
||||
/*
|
||||
Paper-styles color.html dependency is stripped on build.
|
||||
When a default paper-style color is used, it needs to be copied
|
||||
|
@ -31,6 +31,7 @@ export const darkStyles = {
|
||||
"codemirror-property": "#C792EA",
|
||||
"codemirror-qualifier": "#DECB6B",
|
||||
"codemirror-type": "#DECB6B",
|
||||
"energy-grid-return-color": "#b39bdb",
|
||||
};
|
||||
|
||||
export const derivedStyles = {
|
||||
|
@ -390,6 +390,13 @@
|
||||
"failed_create_area": "Failed to create area."
|
||||
}
|
||||
},
|
||||
"statistic-picker": {
|
||||
"statistic": "Statistic",
|
||||
"no_statistics": "You don't have any statistics",
|
||||
"no_match": "No matching statistics found",
|
||||
"missing_entity": "Why is my entity not listed?",
|
||||
"learn_more": "Learn more about statistics"
|
||||
},
|
||||
"addon-picker": {
|
||||
"addon": "Add-on",
|
||||
"error": {
|
||||
@ -992,8 +999,9 @@
|
||||
"description": "Monitor your energy production and consumption",
|
||||
"currency": "",
|
||||
"grid": {
|
||||
"title": "Configure grid",
|
||||
"sub": "Configure the different tarrifs for the energy you consume from the grid, and, if you return energy to the grid, the energy you return to the grid.",
|
||||
"title": "Electricity grid",
|
||||
"sub": "Configure the amount of energy that you consume from the grid and, if you produce energy, give back to the grid. This allows Home Assistant to track your whole home energy usage.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"flow_dialog": {
|
||||
"from": {
|
||||
"header": "Configure grid consumption",
|
||||
@ -1002,7 +1010,7 @@
|
||||
"cost_para": "Select how Home Assistant should keep track of the costs of the consumed energy.",
|
||||
"no_cost": "Do not track costs",
|
||||
"cost_stat": "Use an entity tracking the total costs",
|
||||
"cost_stat_input": "Entity keeping track of the total costs",
|
||||
"cost_stat_input": "Total Costs Entity",
|
||||
"cost_entity": "Use an entity with current price",
|
||||
"cost_entity_input": "Entity with the current price",
|
||||
"cost_number": "Use a static price",
|
||||
@ -1016,7 +1024,7 @@
|
||||
"cost_para": "Do you get money back when you return energy to the grid?",
|
||||
"no_cost": "I do not get money back",
|
||||
"cost_stat": "Use an entity tracking the total recieved money",
|
||||
"cost_stat_input": "Entity keeping track of the total of received money",
|
||||
"cost_stat_input": "Total Compensation Entity",
|
||||
"cost_entity": "Use an entity with current rate",
|
||||
"cost_entity_input": "Entity with the current rate",
|
||||
"cost_number": "Use a static rate",
|
||||
@ -1026,12 +1034,17 @@
|
||||
}
|
||||
},
|
||||
"solar": {
|
||||
"title": "Solar Panels",
|
||||
"sub": "Let Home Assistant monitor your solar panels and give you insight on their performance.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"stat_production": "Your solar energy production",
|
||||
"stat_return_to_grid": "Solar energy returned to the grid",
|
||||
"stat_predicted_production": "Prediction of your solar energy production"
|
||||
},
|
||||
"device_consumption": {
|
||||
"description": "If you measure the power consumption of individual devices, you can select the entities with the power consumption below",
|
||||
"title": "Individual devices",
|
||||
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"add_stat": "Pick entity to track energy of",
|
||||
"selected_stat": "Tracking energy for"
|
||||
}
|
||||
@ -1084,9 +1097,11 @@
|
||||
"unit_system_metric": "Metric",
|
||||
"imperial_example": "Fahrenheit, pounds",
|
||||
"metric_example": "Celsius, kilograms",
|
||||
"find_currency_value": "Find your value",
|
||||
"save_button": "Save",
|
||||
"external_url": "External URL",
|
||||
"internal_url": "Internal URL"
|
||||
"internal_url": "Internal URL",
|
||||
"currency": "Currency"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2688,7 +2703,9 @@
|
||||
"node_status": "Node Status",
|
||||
"node_ready": "Node Ready",
|
||||
"device_config": "Configure Device",
|
||||
"reinterview_device": "Re-interview Device"
|
||||
"reinterview_device": "Re-interview Device",
|
||||
"heal_node": "Heal Node",
|
||||
"remove_failed": "Remove Failed Device"
|
||||
},
|
||||
"node_config": {
|
||||
"header": "Z-Wave Device Configuration",
|
||||
@ -2741,6 +2758,14 @@
|
||||
"exclusion_failed": "The node could not be removed. Please check the logs for more information.",
|
||||
"exclusion_finished": "Node {id} has been removed from your Z-Wave network."
|
||||
},
|
||||
"remove_failed_node": {
|
||||
"title": "Remove a Failed Z-Wave Device",
|
||||
"introduction": "Remove a failed device from your Z-Wave network. Use this if you are unable to exclude a device normally because it is broken.",
|
||||
"remove_device": "Remove Device",
|
||||
"in_progress": "The device removal is in progress.",
|
||||
"removal_finished": "Node {id} has been removed from your Z-Wave network.",
|
||||
"removal_failed": "The device could not be removed from your Z-Wave network."
|
||||
},
|
||||
"reinterview_node": {
|
||||
"title": "Re-interview a Z-Wave Device",
|
||||
"introduction": "Re-interview a device on your Z-Wave network. Use this feature if your device has missing or incorrect functionality.",
|
||||
@ -2763,6 +2788,17 @@
|
||||
"healing_failed": "Healing failed. Additional information may be available in the logs.",
|
||||
"healing_cancelled": "Network healing has been cancelled."
|
||||
},
|
||||
"heal_node": {
|
||||
"title": "Heal a Z-Wave Device",
|
||||
"introduction": "Tell {device} to update its routes back to the controller. This can help with communication issues if you have recently moved the device or your controller.",
|
||||
"traffic_warning": "The healing process generates a large amount of traffic on the Z-Wave network. This may cause devices to respond slowly (or not at all) while the heal is in progress.",
|
||||
"start_heal": "Heal Device",
|
||||
"healing_failed": "{device} could not be healed.",
|
||||
"healing_failed_check_logs": "Additional information may be available in the logs.",
|
||||
"healing_complete": "{device} has been healed.",
|
||||
"in_progress": "{device} healing is in progress.",
|
||||
"network_heal_in_progress": "A Z-Wave network heal is already in progress. Please wait for it to finish before healing an individual device."
|
||||
},
|
||||
"logs": {
|
||||
"title": "Z-Wave JS Logs",
|
||||
"log_level": "Log Level",
|
||||
@ -3059,6 +3095,7 @@
|
||||
},
|
||||
"gauge": {
|
||||
"name": "Gauge",
|
||||
"needle_gauge": "Display as needle gauge?",
|
||||
"severity": {
|
||||
"define": "Define Severity?",
|
||||
"green": "Green",
|
||||
@ -3086,6 +3123,10 @@
|
||||
"name": "History Graph",
|
||||
"description": "The History Graph card allows you to display a graph for each of the entities listed."
|
||||
},
|
||||
"statistics-graph": {
|
||||
"name": "Statistics Graph",
|
||||
"description": "The Statistics Graph card allows you to display a graph of the statistics for each of the entities listed."
|
||||
},
|
||||
"horizontal-stack": {
|
||||
"name": "Horizontal Stack",
|
||||
"description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column."
|
||||
@ -3112,6 +3153,7 @@
|
||||
"entity": "Entity",
|
||||
"hold_action": "Hold Action",
|
||||
"hours_to_show": "Hours to Show",
|
||||
"days_to_show": "Days to Show",
|
||||
"icon": "Icon",
|
||||
"icon_height": "Icon Height",
|
||||
"image": "Image Path",
|
||||
@ -3685,8 +3727,6 @@
|
||||
},
|
||||
"energy": {
|
||||
"setup": {
|
||||
"header": "Setup your energy dashboard",
|
||||
"slogan": "The world is heating up. Together we can fix that.",
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"done": "Show me my energy dashboard!"
|
||||
|
@ -60,6 +60,7 @@ const hassAttributeUtil = {
|
||||
"power",
|
||||
"power_factor",
|
||||
"pressure",
|
||||
"monetary",
|
||||
"signal_strength",
|
||||
"temperature",
|
||||
"timestamp",
|
||||
|
77
test-mocha/data/history.spec.ts
Normal file
77
test-mocha/data/history.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { calculateStatisticsSumGrowthWithPercentage } from "../../src/data/history";
|
||||
|
||||
describe("calculateStatisticsSumGrowthWithPercentage", () => {
|
||||
it("Returns null if not enough values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage([], []),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns null if not enough values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[],
|
||||
]
|
||||
),
|
||||
100
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user