mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-13 14:14:35 +00:00
Compare commits
4 Commits
revert-130
...
editor-hig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aecbfeaaa0 | ||
|
|
498732566e | ||
|
|
b22c51bc2c | ||
|
|
5bc2fd059c |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20220601.0"
|
||||
version = "20220526.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -70,9 +70,7 @@ export const iconColorCSS = css`
|
||||
}
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"],
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LitElement } from "lit";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
@@ -16,21 +15,3 @@ export function computeRTLDirection(hass: HomeAssistant) {
|
||||
export function emitRTLDirection(rtl: boolean) {
|
||||
return rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
export function computeDirectionStyles(isRTL: boolean, element: LitElement) {
|
||||
const direction: string = emitRTLDirection(isRTL);
|
||||
setDirectionStyles(direction, element);
|
||||
}
|
||||
|
||||
export function setDirectionStyles(direction: string, element: LitElement) {
|
||||
element.style.direction = direction;
|
||||
element.style.setProperty("--direction", direction);
|
||||
element.style.setProperty(
|
||||
"--float-start",
|
||||
direction === "ltr" ? "left" : "right"
|
||||
);
|
||||
element.style.setProperty(
|
||||
"--float-end",
|
||||
direction === "ltr" ? "right" : "left"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,26 +37,6 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._releaseCanvas();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setupChart();
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
|
||||
@@ -28,11 +28,11 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
@state() private _chartOptions?: ChartOptions<"line">;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -57,7 +57,6 @@ class StateHistoryChartLine extends LitElement {
|
||||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
@@ -131,11 +130,28 @@ class StateHistoryChartLine extends LitElement {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const entityStates = this.data;
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (entityStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = this.endTime;
|
||||
endTime =
|
||||
this.endTime ||
|
||||
// Get the highest date from the last date of each device
|
||||
new Date(
|
||||
Math.max(
|
||||
...entityStates.map((devSts) =>
|
||||
new Date(
|
||||
devSts.states[devSts.states.length - 1].last_changed
|
||||
).getTime()
|
||||
)
|
||||
)
|
||||
);
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
const names = this.names || {};
|
||||
entityStates.forEach((states) => {
|
||||
const domain = states.domain;
|
||||
|
||||
@@ -83,8 +83,6 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public names: boolean | Record<string, string> = false;
|
||||
|
||||
@property() public unit?: string;
|
||||
@@ -93,11 +91,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ type: Boolean }) public chunked = false;
|
||||
|
||||
@property({ attribute: false }) public startTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"timeline">;
|
||||
|
||||
@@ -116,7 +110,6 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
const narrow = this.narrow;
|
||||
this._chartOptions = {
|
||||
maintainAspectRatio: false,
|
||||
parsing: false,
|
||||
@@ -130,8 +123,6 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMin: this.startTime,
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
@@ -162,18 +153,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display:
|
||||
this.chunked || !this.isSingleDevice || this.data.length !== 1,
|
||||
display: this.data.length !== 1,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
},
|
||||
afterFit: (scaleInstance) => {
|
||||
if (this.chunked) {
|
||||
// ensure all the chart labels are the same width
|
||||
scaleInstance.width = narrow ? 105 : 185;
|
||||
}
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
},
|
||||
},
|
||||
@@ -224,8 +208,34 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
stateHistory = [];
|
||||
}
|
||||
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const startTime = new Date(
|
||||
stateHistory.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
// end time is Math.max(startTime, last_event)
|
||||
let endTime =
|
||||
this.endTime ||
|
||||
new Date(
|
||||
stateHistory.reduce(
|
||||
(maxTime, stateInfo) =>
|
||||
Math.max(
|
||||
maxTime,
|
||||
new Date(
|
||||
stateInfo.data[stateInfo.data.length - 1].last_changed
|
||||
).getTime()
|
||||
),
|
||||
startTime.getTime()
|
||||
)
|
||||
);
|
||||
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
const labels: string[] = [];
|
||||
const datasets: ChartDataset<"timeline">[] = [];
|
||||
const names = this.names || {};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -7,29 +6,12 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state, eventOptions } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
} from "../../data/history";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
const chunkData = (inputArray: any[], chunks: number) =>
|
||||
inputArray.reduce((results, item, idx) => {
|
||||
const chunkIdx = Math.floor(idx / chunks);
|
||||
if (!results[chunkIdx]) {
|
||||
results[chunkIdx] = [];
|
||||
}
|
||||
results[chunkIdx].push(item);
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
@customElement("state-history-charts")
|
||||
class StateHistoryCharts extends LitElement {
|
||||
@@ -37,13 +19,8 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public historyData!: HistoryResult;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public names = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
||||
public virtualize = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
@@ -52,104 +29,59 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@state() private _computedStartTime!: Date;
|
||||
|
||||
@state() private _computedEndTime!: Date;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html`<div class="info">
|
||||
return html` <div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this.isLoadingData && !this.historyData) {
|
||||
return html`<div class="info">
|
||||
return html` <div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.loading_history")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._isHistoryEmpty()) {
|
||||
return html`<div class="info">
|
||||
return html` <div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.no_history_found")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const computedEndTime = this.upToNow
|
||||
? new Date()
|
||||
: this.endTime || new Date();
|
||||
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
const combinedItems = this.historyData.timeline.length
|
||||
? (this.virtualize
|
||||
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
|
||||
: [this.historyData.timeline]
|
||||
).concat(this.historyData.line)
|
||||
: this.historyData.line;
|
||||
|
||||
return this.virtualize
|
||||
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${combinedItems}
|
||||
.renderItem=${this._renderHistoryItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`;
|
||||
return html`
|
||||
${this.historyData.timeline.length
|
||||
? html`
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${this.historyData.timeline}
|
||||
.endTime=${computedEndTime}
|
||||
.noSingle=${this.noSingle}
|
||||
.names=${this.names}
|
||||
></state-history-chart-timeline>
|
||||
`
|
||||
: html``}
|
||||
${this.historyData.line.map(
|
||||
(line) => html`
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${line.unit}
|
||||
.data=${line.data}
|
||||
.identifier=${line.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
line.data &&
|
||||
line.data.length === 1}
|
||||
.endTime=${computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHistoryItem = (
|
||||
item: TimelineEntity[] | LineChartUnit,
|
||||
index: number
|
||||
): TemplateResult => {
|
||||
if (!item || index === undefined) {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.line?.length === 1}
|
||||
.endTime=${this._computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.timeline?.length === 1}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return !(changedProps.size === 1 && changedProps.has("hass"));
|
||||
}
|
||||
@@ -164,11 +96,6 @@ class StateHistoryCharts extends LitElement {
|
||||
return !this.isLoadingData && historyDataEmpty;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
@@ -176,47 +103,11 @@ class StateHistoryCharts extends LitElement {
|
||||
/* height of single timeline chart = 60px */
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.container {
|
||||
max-height: var(--history-max-height);
|
||||
}
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:host([virtualize]) .entry-container {
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.container,
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
state-history-chart-timeline,
|
||||
state-history-chart-line {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,6 @@ export class HaTextSelector extends LitElement {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ export class HaTextField extends TextFieldBase {
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start);
|
||||
direction: var(--direction);
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
|
||||
@@ -43,11 +43,7 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
@@ -567,8 +563,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<div
|
||||
class="${["app", "directory"].includes(child.media_class)
|
||||
? "centered-image"
|
||||
: ""} ${isBrandUrl(child.thumbnail)
|
||||
? "brand-image"
|
||||
: ""} image"
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
@@ -667,7 +661,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
@@ -1056,10 +1050,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
background-size: 40%;
|
||||
}
|
||||
|
||||
.children ha-card .icon-holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -240,7 +240,6 @@ export interface EnergyData {
|
||||
prefs: EnergyPreferences;
|
||||
info: EnergyInfo;
|
||||
stats: Statistics;
|
||||
statsMetadata: Record<string, StatisticsMetaData>;
|
||||
statsCompare: Statistics;
|
||||
co2SignalConfigEntry?: ConfigEntry;
|
||||
co2SignalEntity?: string;
|
||||
@@ -286,6 +285,15 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
const statIDs: string[] = [];
|
||||
const gasSources: GasSourceTypeEnergyPreference[] =
|
||||
prefs.energy_sources.filter(
|
||||
(source) => source.type === "gas"
|
||||
) as GasSourceTypeEnergyPreference[];
|
||||
const gasStatisticIdsWithMeta: StatisticsMetaData[] =
|
||||
await getStatisticMetadata(
|
||||
hass,
|
||||
gasSources.map((source) => source.stat_energy_from)
|
||||
);
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
@@ -295,6 +303,20 @@ const getEnergyData = async (
|
||||
|
||||
if (source.type === "gas") {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (!entity) {
|
||||
for (const statisticIdWithMeta of gasStatisticIdsWithMeta) {
|
||||
if (
|
||||
statisticIdWithMeta?.statistic_id === source.stat_energy_from &&
|
||||
statisticIdWithMeta?.unit_of_measurement
|
||||
) {
|
||||
source.unit_of_measurement =
|
||||
statisticIdWithMeta?.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta?.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
}
|
||||
@@ -410,12 +432,6 @@ const getEnergyData = async (
|
||||
}
|
||||
});
|
||||
|
||||
const statsMetadataArray = await getStatisticMetadata(hass, statIDs);
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
|
||||
const data: EnergyData = {
|
||||
start,
|
||||
end,
|
||||
@@ -424,7 +440,6 @@ const getEnergyData = async (
|
||||
info,
|
||||
prefs,
|
||||
stats,
|
||||
statsMetadata,
|
||||
statsCompare,
|
||||
co2SignalConfigEntry,
|
||||
co2SignalEntity,
|
||||
@@ -613,13 +628,13 @@ export const getEnergyGasUnitCategory = (
|
||||
|
||||
export const getEnergyGasUnit = (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData> = {}
|
||||
prefs: EnergyPreferences
|
||||
): string | undefined => {
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type !== "gas") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (entity?.attributes.unit_of_measurement) {
|
||||
// Wh is normalized to kWh by stats generation
|
||||
@@ -627,11 +642,8 @@ export const getEnergyGasUnit = (
|
||||
? "kWh"
|
||||
: entity.attributes.unit_of_measurement;
|
||||
}
|
||||
const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
|
||||
if (statisticIdWithMeta?.unit_of_measurement) {
|
||||
return statisticIdWithMeta.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta.unit_of_measurement;
|
||||
if (source.unit_of_measurement) {
|
||||
return source.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||
import {
|
||||
computeStateName,
|
||||
computeStateNameFromEntityAttributes,
|
||||
} from "../common/entity/compute_state_name";
|
||||
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
@@ -550,16 +547,3 @@ export const adjustStatisticsSum = (
|
||||
start_time,
|
||||
adjustment,
|
||||
});
|
||||
|
||||
export const getStatisticLabel = (
|
||||
hass: HomeAssistant,
|
||||
statisticsId: string,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>
|
||||
): string => {
|
||||
const entity = hass.states[statisticsId];
|
||||
if (entity) {
|
||||
return computeStateName(entity);
|
||||
}
|
||||
const statisticMetaData = statisticsMetaData[statisticsId];
|
||||
return statisticMetaData?.name || statisticsId;
|
||||
};
|
||||
|
||||
@@ -51,15 +51,11 @@ function initialize(
|
||||
const style = document.createElement("style");
|
||||
|
||||
style.innerHTML = `
|
||||
body {
|
||||
margin:0;
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
}
|
||||
body { margin:0; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
color: var(--primary-text-color, #e1e1e1);
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -3,8 +3,6 @@ import { property } from "lit/decorators";
|
||||
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
|
||||
import { Constructor, Resources } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
|
||||
const empty = () => "";
|
||||
|
||||
@@ -27,14 +25,6 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.get("translationFragment")) {
|
||||
|
||||
@@ -315,8 +315,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-textfield,
|
||||
ha-blueprint-picker {
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
h3 {
|
||||
|
||||
@@ -106,9 +106,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
const offsetType = ev.detail.value.offset_type === "before" ? "-" : "";
|
||||
const newTrigger = {
|
||||
...ev.detail.value,
|
||||
offset: `${offsetType}${duration.hours ?? 0}:${duration.minutes ?? 0}:${
|
||||
duration.seconds ?? 0
|
||||
}`,
|
||||
offset: `${offsetType}${duration.hours}:${duration.minutes}:${duration.seconds}`,
|
||||
};
|
||||
delete newTrigger.offset_type;
|
||||
fireEvent(this, "value-changed", { value: newTrigger });
|
||||
|
||||
@@ -125,7 +125,6 @@ class HaConfigHardware extends LitElement {
|
||||
<div class="card-content">
|
||||
<mwc-list>
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
graphic=${ifDefined(imageURL ? "medium" : undefined)}
|
||||
.twoline=${Boolean(boardId)}
|
||||
>
|
||||
|
||||
@@ -80,44 +80,44 @@ class HaPanelHistory extends LitElement {
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
.hass=${this.hass}
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
.ranges=${this._ranges}
|
||||
@change=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
<div class="flex content">
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
.hass=${this.hass}
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
.ranges=${this._ranges}
|
||||
@change=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.entity"
|
||||
)}
|
||||
.disabled=${this._isLoading}
|
||||
@change=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.entity"
|
||||
)}
|
||||
.disabled=${this._isLoading}
|
||||
@change=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`
|
||||
<state-history-charts
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`
|
||||
<state-history-charts
|
||||
virtualize
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
.narrow=${this.narrow}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
@@ -235,14 +235,6 @@ class HaPanelHistory extends LitElement {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
state-history-charts {
|
||||
height: calc(100vh - 136px);
|
||||
}
|
||||
|
||||
:host([narrow]) state-history-charts {
|
||||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
height: calc(100vh - 136px);
|
||||
}
|
||||
@@ -251,10 +243,6 @@ class HaPanelHistory extends LitElement {
|
||||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ export class HaLogbook extends LitElement {
|
||||
// Put newest ones on top. Reverse works in-place so
|
||||
// make a copy first.
|
||||
const newEntries = [...streamMessage.events].reverse();
|
||||
if (!this._logbookEntries || !this._logbookEntries.length) {
|
||||
if (!this._logbookEntries) {
|
||||
this._logbookEntries = newEntries;
|
||||
return;
|
||||
}
|
||||
@@ -320,16 +320,14 @@ export class HaLogbook extends LitElement {
|
||||
return;
|
||||
}
|
||||
const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime);
|
||||
this._logbookEntries = !nonExpiredRecords.length
|
||||
? // All existing entries expired
|
||||
newEntries
|
||||
: newEntries[0].when >= nonExpiredRecords[0].when
|
||||
? // The new records are newer than the old records
|
||||
// append the old records to the end of the new records
|
||||
newEntries.concat(nonExpiredRecords)
|
||||
: // The new records are older than the old records
|
||||
// append the new records to the end of the old records
|
||||
nonExpiredRecords.concat(newEntries);
|
||||
this._logbookEntries =
|
||||
newEntries[0].when >= this._logbookEntries[0].when
|
||||
? // The new records are newer than the old records
|
||||
// append the old records to the end of the new records
|
||||
newEntries.concat(nonExpiredRecords)
|
||||
: // The new records are older than the old records
|
||||
// append the new records to the end of the old records
|
||||
nonExpiredRecords.concat(newEntries);
|
||||
};
|
||||
|
||||
private _updateTraceContexts = throttle(async () => {
|
||||
|
||||
@@ -43,6 +43,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
|
||||
@state() private _config?: EnergyDevicesGraphCardConfig;
|
||||
|
||||
@state() private _data?: Statistics;
|
||||
|
||||
@state() private _chartData: ChartData = { datasets: [] };
|
||||
|
||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
||||
@@ -160,24 +162,19 @@ export class HuiEnergyDevicesGraphCard
|
||||
energyData.start
|
||||
);
|
||||
|
||||
const devices = energyData.prefs.device_consumption.map(
|
||||
(device) => device.stat_consumption
|
||||
this._data = await fetchStatistics(
|
||||
this.hass,
|
||||
addHours(energyData.start, -1),
|
||||
energyData.end,
|
||||
energyData.prefs.device_consumption.map(
|
||||
(device) => device.stat_consumption
|
||||
),
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
|
||||
const period =
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||
|
||||
const startMinHour = addHours(energyData.start, -1);
|
||||
|
||||
const data = await fetchStatistics(
|
||||
this.hass,
|
||||
startMinHour,
|
||||
energyData.end,
|
||||
devices,
|
||||
period
|
||||
);
|
||||
|
||||
Object.values(data).forEach((stat) => {
|
||||
Object.values(this._data).forEach((stat) => {
|
||||
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
stat.unshift({
|
||||
@@ -190,41 +187,9 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
});
|
||||
|
||||
let compareData: Statistics | undefined;
|
||||
|
||||
if (energyData.startCompare && energyData.endCompare) {
|
||||
const startCompareMinHour = addHours(energyData.startCompare, -1);
|
||||
compareData = await fetchStatistics(
|
||||
this.hass,
|
||||
startCompareMinHour,
|
||||
energyData.endCompare,
|
||||
devices,
|
||||
period
|
||||
);
|
||||
|
||||
Object.values(compareData).forEach((stat) => {
|
||||
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
stat.unshift({
|
||||
...stat[0],
|
||||
start: startCompareMinHour.toISOString(),
|
||||
end: startCompareMinHour.toISOString(),
|
||||
sum: 0,
|
||||
state: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const chartData: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> =
|
||||
[];
|
||||
const chartDataCompare: Array<
|
||||
ChartDataset<"bar", ParsedDataType<"bar">>["data"]
|
||||
> = [];
|
||||
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];
|
||||
const borderColor: string[] = [];
|
||||
const borderColorCompare: string[] = [];
|
||||
const backgroundColor: string[] = [];
|
||||
const backgroundColorCompare: string[] = [];
|
||||
|
||||
const datasets: ChartDataset<"bar", ParsedDataType<"bar">[]>[] = [
|
||||
{
|
||||
@@ -233,69 +198,35 @@ export class HuiEnergyDevicesGraphCard
|
||||
),
|
||||
borderColor,
|
||||
backgroundColor,
|
||||
data: chartData,
|
||||
barThickness: compareData ? 10 : 20,
|
||||
data,
|
||||
barThickness: 20,
|
||||
},
|
||||
];
|
||||
|
||||
if (compareData) {
|
||||
datasets.push({
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage"
|
||||
),
|
||||
borderColor: borderColorCompare,
|
||||
backgroundColor: backgroundColorCompare,
|
||||
data: chartDataCompare,
|
||||
barThickness: 10,
|
||||
});
|
||||
}
|
||||
|
||||
energyData.prefs.device_consumption.forEach((device, idx) => {
|
||||
const value =
|
||||
device.stat_consumption in data
|
||||
? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
|
||||
device.stat_consumption in this._data!
|
||||
? calculateStatisticSumGrowth(this._data![device.stat_consumption]) ||
|
||||
0
|
||||
: 0;
|
||||
|
||||
chartData.push({
|
||||
data.push({
|
||||
// @ts-expect-error
|
||||
y: device.stat_consumption,
|
||||
x: value,
|
||||
idx,
|
||||
});
|
||||
|
||||
if (compareData) {
|
||||
const compareValue =
|
||||
device.stat_consumption in compareData
|
||||
? calculateStatisticSumGrowth(
|
||||
compareData[device.stat_consumption]
|
||||
) || 0
|
||||
: 0;
|
||||
|
||||
chartDataCompare.push({
|
||||
// @ts-expect-error
|
||||
y: device.stat_consumption,
|
||||
x: compareValue,
|
||||
idx,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chartData.sort((a, b) => b.x - a.x);
|
||||
data.sort((a, b) => b.x - a.x);
|
||||
|
||||
chartData.forEach((d: any) => {
|
||||
data.forEach((d: any) => {
|
||||
const color = getColorByIndex(d.idx);
|
||||
|
||||
borderColor.push(color);
|
||||
backgroundColor.push(color + "7F");
|
||||
});
|
||||
|
||||
chartDataCompare.forEach((d: any) => {
|
||||
const color = getColorByIndex(d.idx);
|
||||
|
||||
borderColorCompare.push(color + "7F");
|
||||
backgroundColorCompare.push(color + "32");
|
||||
});
|
||||
|
||||
this._chartData = {
|
||||
datasets,
|
||||
};
|
||||
|
||||
@@ -315,11 +315,7 @@ class HuiEnergyDistrubutionCard
|
||||
${formatNumber(gasUsage || 0, this.hass.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
${getEnergyGasUnit(
|
||||
this.hass,
|
||||
prefs,
|
||||
this._data.statsMetadata
|
||||
) || "m³"}
|
||||
${getEnergyGasUnit(this.hass, prefs) || "m³"}
|
||||
</div>
|
||||
<svg width="80" height="30">
|
||||
<path d="M40 0 v30" id="gas" />
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
@@ -38,11 +39,7 @@ import {
|
||||
getEnergyDataCollection,
|
||||
getEnergyGasUnit,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticLabel,
|
||||
} from "../../../../data/history";
|
||||
import { Statistics } from "../../../../data/history";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -273,9 +270,7 @@ export class HuiEnergyGasGraphCard
|
||||
(source) => source.type === "gas"
|
||||
) as GasSourceTypeEnergyPreference[];
|
||||
|
||||
this._unit =
|
||||
getEnergyGasUnit(this.hass, energyData.prefs, energyData.statsMetadata) ||
|
||||
"m³";
|
||||
this._unit = getEnergyGasUnit(this.hass, energyData.prefs) || "m³";
|
||||
|
||||
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||
|
||||
@@ -285,12 +280,7 @@ export class HuiEnergyGasGraphCard
|
||||
.trim();
|
||||
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.stats,
|
||||
energyData.statsMetadata,
|
||||
gasSources,
|
||||
gasColor
|
||||
)
|
||||
...this._processDataSet(energyData.stats, gasSources, gasColor)
|
||||
);
|
||||
|
||||
if (energyData.statsCompare) {
|
||||
@@ -308,7 +298,6 @@ export class HuiEnergyGasGraphCard
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.statsCompare,
|
||||
energyData.statsMetadata,
|
||||
gasSources,
|
||||
gasColor,
|
||||
true
|
||||
@@ -329,14 +318,14 @@ export class HuiEnergyGasGraphCard
|
||||
|
||||
private _processDataSet(
|
||||
statistics: Statistics,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||
gasSources: GasSourceTypeEnergyPreference[],
|
||||
gasColor: string,
|
||||
compare = false
|
||||
) {
|
||||
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||
|
||||
gasSources.forEach((source, idx) => {
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
|
||||
const modifiedColor =
|
||||
idx > 0
|
||||
? this.hass.themes.darkMode
|
||||
@@ -379,11 +368,7 @@ export class HuiEnergyGasGraphCard
|
||||
}
|
||||
|
||||
data.push({
|
||||
label: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData
|
||||
),
|
||||
label: entity ? computeStateName(entity) : source.stat_energy_from,
|
||||
borderColor: compare ? borderColor + "7F" : borderColor,
|
||||
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
|
||||
data: gasConsumptionData,
|
||||
|
||||
@@ -40,11 +40,7 @@ import {
|
||||
getEnergySolarForecasts,
|
||||
SolarSourceTypeEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticLabel,
|
||||
} from "../../../../data/history";
|
||||
import { Statistics } from "../../../../data/history";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -293,12 +289,7 @@ export class HuiEnergySolarGraphCard
|
||||
.trim();
|
||||
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.stats,
|
||||
energyData.statsMetadata,
|
||||
solarSources,
|
||||
solarColor
|
||||
)
|
||||
...this._processDataSet(energyData.stats, solarSources, solarColor)
|
||||
);
|
||||
|
||||
if (energyData.statsCompare) {
|
||||
@@ -316,7 +307,6 @@ export class HuiEnergySolarGraphCard
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.statsCompare,
|
||||
energyData.statsMetadata,
|
||||
solarSources,
|
||||
solarColor,
|
||||
true
|
||||
@@ -349,7 +339,6 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
private _processDataSet(
|
||||
statistics: Statistics,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||
solarSources: SolarSourceTypeEnergyPreference[],
|
||||
solarColor: string,
|
||||
compare = false
|
||||
@@ -357,6 +346,8 @@ export class HuiEnergySolarGraphCard
|
||||
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
|
||||
|
||||
solarSources.forEach((source, idx) => {
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
|
||||
const modifiedColor =
|
||||
idx > 0
|
||||
? this.hass.themes.darkMode
|
||||
@@ -402,11 +393,7 @@ export class HuiEnergySolarGraphCard
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_solar_graph.production",
|
||||
{
|
||||
name: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
statisticsMetaData
|
||||
),
|
||||
name: entity ? computeStateName(entity) : source.stat_energy_from,
|
||||
}
|
||||
),
|
||||
borderColor: compare ? borderColor + "7F" : borderColor,
|
||||
|
||||
@@ -128,9 +128,7 @@ export class HuiEnergySourcesTableCard
|
||||
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
|
||||
);
|
||||
|
||||
const gasUnit =
|
||||
getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
|
||||
"";
|
||||
const gasUnit = getEnergyGasUnit(this.hass, this._data.prefs) || "";
|
||||
|
||||
const compare = this._data.statsCompare !== undefined;
|
||||
|
||||
@@ -851,9 +849,7 @@ export class HuiEnergySourcesTableCard
|
||||
)}
|
||||
</th>
|
||||
${compare
|
||||
? html`<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
>
|
||||
? html`<td class="mdc-data-table__cell">
|
||||
${formatNumber(
|
||||
totalGasCostCompare + totalGridCostCompare,
|
||||
this.hass.locale,
|
||||
@@ -864,7 +860,9 @@ export class HuiEnergySourcesTableCard
|
||||
)}
|
||||
</td>
|
||||
${showCosts
|
||||
? html`<td class="mdc-data-table__cell"></td>`
|
||||
? html`<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||
></td>`
|
||||
: ""}`
|
||||
: ""}
|
||||
<td class="mdc-data-table__cell"></td>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
@@ -33,11 +34,7 @@ import {
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticLabel,
|
||||
} from "../../../../data/history";
|
||||
import { Statistics } from "../../../../data/history";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -381,14 +378,7 @@ export class HuiEnergyUsageGraphCard
|
||||
this._compareEnd = energyData.endCompare;
|
||||
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.stats,
|
||||
energyData.statsMetadata,
|
||||
statIds,
|
||||
colors,
|
||||
labels,
|
||||
false
|
||||
)
|
||||
...this._processDataSet(energyData.stats, statIds, colors, labels, false)
|
||||
);
|
||||
|
||||
if (energyData.statsCompare) {
|
||||
@@ -406,7 +396,6 @@ export class HuiEnergyUsageGraphCard
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.statsCompare,
|
||||
energyData.statsMetadata,
|
||||
statIds,
|
||||
colors,
|
||||
labels,
|
||||
@@ -422,7 +411,6 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
private _processDataSet(
|
||||
statistics: Statistics,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||
statIdsByCat: {
|
||||
to_grid?: string[] | undefined;
|
||||
from_grid?: string[] | undefined;
|
||||
@@ -592,6 +580,8 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
Object.entries(sources).forEach(([statId, source], idx) => {
|
||||
const entity = this.hass.states[statId];
|
||||
|
||||
const modifiedColor =
|
||||
idx > 0
|
||||
? this.hass.themes.darkMode
|
||||
@@ -620,7 +610,9 @@ export class HuiEnergyUsageGraphCard
|
||||
label:
|
||||
type in labels
|
||||
? labels[type]
|
||||
: getStatisticLabel(this.hass, statId, statisticsMetaData),
|
||||
: entity
|
||||
? computeStateName(entity)
|
||||
: statId,
|
||||
order:
|
||||
type === "used_solar"
|
||||
? 1
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -54,6 +55,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
return { type: "entities", entities: foundEntities };
|
||||
}
|
||||
|
||||
@property() public editMode?: boolean | any;
|
||||
|
||||
@state() private _config?: EntitiesCardConfig;
|
||||
|
||||
private _hass?: HomeAssistant;
|
||||
@@ -217,9 +220,15 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
`}
|
||||
</h1>
|
||||
`}
|
||||
<div id="states" class="card-content">
|
||||
${this._configEntities!.map((entityConf) =>
|
||||
this.renderEntity(entityConf)
|
||||
<div
|
||||
id="states"
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
highlight: this.editMode?.selectedRow !== undefined,
|
||||
})}
|
||||
>
|
||||
${this._configEntities!.map((entityConf, idx) =>
|
||||
this.renderEntity(entityConf, idx)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -272,6 +281,12 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
#states > div {
|
||||
position: relative;
|
||||
}
|
||||
#states.highlight > div.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
#states.highlight > div {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0px 18px 0px 8px;
|
||||
@@ -293,7 +308,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
|
||||
private renderEntity(
|
||||
entityConf: LovelaceRowConfig,
|
||||
idx: number
|
||||
): TemplateResult {
|
||||
const element = createRowElement(
|
||||
(!("type" in entityConf) || entityConf.type === "conditional") &&
|
||||
this._config!.state_color
|
||||
@@ -307,7 +325,13 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
|
||||
return html`<div>${element}</div>`;
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
selected: this.editMode?.selectedRow === idx,
|
||||
})}
|
||||
>
|
||||
${element}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
@@ -28,7 +29,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public editMode?: boolean;
|
||||
@property() public editMode?: any;
|
||||
|
||||
@property() protected _cards?: LovelaceCard[];
|
||||
|
||||
@@ -43,8 +44,16 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
this._cards = config.cards.map((card) => {
|
||||
this._cards = config.cards.map((card, idx) => {
|
||||
const element = this._createCardElement(card) as LovelaceCard;
|
||||
if (this.editMode !== undefined) {
|
||||
if (this.editMode?.selected === idx) {
|
||||
element.classList.add("selected");
|
||||
element.editMode = this.editMode.data;
|
||||
} else {
|
||||
element.editMode = true;
|
||||
}
|
||||
}
|
||||
return element;
|
||||
});
|
||||
}
|
||||
@@ -58,12 +67,18 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of this._cards) {
|
||||
for (const [idx, element] of this._cards.entries()) {
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
if (this.editMode !== undefined) {
|
||||
element.editMode = this.editMode;
|
||||
if (this.editMode.selected === idx) {
|
||||
element.editMode = this.editMode.data ?? true;
|
||||
element.classList.add("selected");
|
||||
} else {
|
||||
element.editMode = true;
|
||||
element.classList.remove("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +92,12 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div id="root">${this._cards}</div>
|
||||
<div
|
||||
id="root"
|
||||
class=${classMap({ highlight: this.editMode?.selected !== undefined })}
|
||||
>
|
||||
${this._cards}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -95,6 +115,12 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
display: block;
|
||||
padding: 24px 16px 16px;
|
||||
}
|
||||
#root.highlight > *.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
#root.highlight > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiCompare,
|
||||
mdiCompareRemove,
|
||||
} from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
@@ -45,15 +40,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public collectionKey?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() _startDate?: Date;
|
||||
|
||||
@state() _endDate?: Date;
|
||||
|
||||
@state() private _period?: "day" | "week" | "month" | "year";
|
||||
|
||||
@state() private _compare = false;
|
||||
@state() private _compare? = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -143,24 +136,14 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
dense
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
${this.narrow
|
||||
? html`<ha-icon-button
|
||||
class="compare ${this._compare ? "active" : ""}"
|
||||
.path=${this._compare ? mdiCompareRemove : mdiCompare}
|
||||
@click=${this._toggleCompare}
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
Compare data
|
||||
</ha-icon-button>`
|
||||
: html`<mwc-button
|
||||
class="compare ${this._compare ? "active" : ""}"
|
||||
@click=${this._toggleCompare}
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
Compare data
|
||||
</mwc-button>`}
|
||||
<mwc-button
|
||||
class="compare ${this._compare ? "active" : ""}"
|
||||
@click=${this._toggleCompare}
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
Compare data
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -277,6 +260,9 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
:host([narrow]) .row {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
:host([narrow]) .period {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.label {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -289,17 +275,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
:host([narrow]) .period {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-icon-button {
|
||||
margin-left: 4px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-icon-button.active::before,
|
||||
mwc-button.active::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -313,11 +288,14 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
ha-icon-button.active::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.compare {
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
width: max-content;
|
||||
}
|
||||
:host([narrow]) .compare {
|
||||
margin-left: auto;
|
||||
margin-top: 8px;
|
||||
}
|
||||
:host {
|
||||
--mdc-button-outline-color: currentColor;
|
||||
|
||||
@@ -12,6 +12,8 @@ export class HuiCardPreview extends ReactiveElement {
|
||||
|
||||
@property() public config?: LovelaceCardConfig;
|
||||
|
||||
@property() public editMode = true;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
|
||||
private get _error() {
|
||||
@@ -81,6 +83,9 @@ export class HuiCardPreview extends ReactiveElement {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("editMode")) {
|
||||
this._element!.editMode = this.editMode;
|
||||
}
|
||||
}
|
||||
|
||||
private _createCard(configValue: LovelaceCardConfig): void {
|
||||
@@ -90,6 +95,7 @@ export class HuiCardPreview extends ReactiveElement {
|
||||
if (this.hass) {
|
||||
this._element!.hass = this.hass;
|
||||
}
|
||||
this._element!.editMode = this.editMode;
|
||||
|
||||
this.appendChild(this._element!);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ declare global {
|
||||
interface HTMLElementEventMap {
|
||||
"reload-lovelace": HASSDomEvent<undefined>;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"edit-mode-changed": any;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"edit-mode-changed": HASSDomEvent<any>;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-edit-card")
|
||||
@@ -77,10 +84,13 @@ export class HuiDialogEditCard
|
||||
|
||||
@state() private _isEscapeEnabled = true;
|
||||
|
||||
@state() private _editMode = true;
|
||||
|
||||
public async showDialog(params: EditCardDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
this._editMode = true;
|
||||
const [view, card] = params.path;
|
||||
this._viewConfig = params.lovelaceConfig.views[view];
|
||||
this._cardConfig =
|
||||
@@ -205,6 +215,7 @@ export class HuiDialogEditCard
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@edit-mode-changed=${this._handleEditModeChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
dialogInitialFocus
|
||||
@@ -214,6 +225,7 @@ export class HuiDialogEditCard
|
||||
<hui-card-preview
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
.editMode=${this._editMode}
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card-preview>
|
||||
${this._error
|
||||
@@ -284,6 +296,10 @@ export class HuiDialogEditCard
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _handleEditModeChanged(ev: HASSDomEvent<any>) {
|
||||
this._editMode = ev.detail ?? true;
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
this._GUImode = ev.detail.guiMode;
|
||||
|
||||
@@ -408,10 +408,15 @@ export class HuiEntitiesCardEditor
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
|
||||
this._subElementEditorConfig = ev.detail.subElementConfig;
|
||||
|
||||
fireEvent(this, "edit-mode-changed", {
|
||||
selectedRow: ev.detail.subElementConfig?.index,
|
||||
});
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
this._subElementEditorConfig = undefined;
|
||||
fireEvent(this, "edit-mode-changed", true);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -144,6 +144,7 @@ export class HuiStackCardEditor
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@edit-mode-changed=${this._handleEditModeChanged}
|
||||
></hui-card-element-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -166,6 +167,9 @@ export class HuiStackCardEditor
|
||||
this._setMode(true);
|
||||
this._guiModeAvailable = true;
|
||||
this._selectedCard = parseInt(ev.detail.selected, 10);
|
||||
if (this._cardEditorEl) {
|
||||
this._cardEditorEl.forceRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
@@ -226,6 +230,14 @@ export class HuiStackCardEditor
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
}
|
||||
|
||||
protected _handleEditModeChanged(ev: HASSDomEvent<any>) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "edit-mode-changed", {
|
||||
selected: this._selectedCard,
|
||||
data: ev.detail,
|
||||
});
|
||||
}
|
||||
|
||||
protected _toggleMode(): void {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
}
|
||||
@@ -237,6 +249,13 @@ export class HuiStackCardEditor
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("_selectedCard"))
|
||||
fireEvent(this, "edit-mode-changed", {
|
||||
selected: this._selectedCard,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
|
||||
@@ -53,6 +53,8 @@ export abstract class HuiElementEditor<T> extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
public forceRebuild = false;
|
||||
|
||||
@state() private _yaml?: string;
|
||||
|
||||
@state() private _config?: T;
|
||||
@@ -292,7 +294,11 @@ export abstract class HuiElementEditor<T> extends LitElement {
|
||||
this._errors = undefined;
|
||||
this._warnings = undefined;
|
||||
|
||||
if (this._configElementType !== this.configElementType) {
|
||||
if (
|
||||
this._configElementType !== this.configElementType ||
|
||||
this.forceRebuild
|
||||
) {
|
||||
this.forceRebuild = false;
|
||||
// If the type has changed, we need to load a new GUI editor
|
||||
this._guiSupported = undefined;
|
||||
this._configElement = undefined;
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface LovelaceBadge extends HTMLElement {
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
editMode?: any;
|
||||
getCardSize(): number | Promise<number>;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { atLeastVersion } from "../common/config/version";
|
||||
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
computeRTLDirection,
|
||||
setDirectionStyles,
|
||||
} from "../common/util/compute_rtl";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import {
|
||||
getHassTranslations,
|
||||
@@ -191,8 +188,17 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
|
||||
private _applyDirection(hass: HomeAssistant) {
|
||||
const direction = computeRTLDirection(hass);
|
||||
this.style.direction = direction;
|
||||
document.dir = direction;
|
||||
setDirectionStyles(direction, this);
|
||||
this.style.setProperty("--direction", direction);
|
||||
this.style.setProperty(
|
||||
"--float-start",
|
||||
direction === "ltr" ? "left" : "right"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--float-end",
|
||||
direction === "ltr" ? "right" : "left"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2376,7 +2376,6 @@
|
||||
"instance_is_available": "Your instance is available at your",
|
||||
"instance_will_be_available": "Your instance will be available at your",
|
||||
"link_learn_how_it_works": "Learn how it works",
|
||||
"nabu_casa_url": "Nabu Casa URL",
|
||||
"certificate_info": "Certificate Info"
|
||||
},
|
||||
"alexa": {
|
||||
@@ -3405,8 +3404,7 @@
|
||||
"go_to_energy_dashboard": "Go to the energy dashboard"
|
||||
},
|
||||
"energy_devices_graph": {
|
||||
"energy_usage": "Energy usage",
|
||||
"previous_energy_usage": "Previous energy usage"
|
||||
"energy_usage": "Energy usage"
|
||||
},
|
||||
"carbon_consumed_gauge": {
|
||||
"card_indicates_energy_used": "This card indicates how much of the energy consumed by your home was generated using non-fossil fuels like solar, wind and nuclear. The higher, the better!",
|
||||
|
||||
@@ -23,6 +23,3 @@ export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
|
||||
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
|
||||
|
||||
export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4];
|
||||
|
||||
export const isBrandUrl = (thumbnail: string | ""): boolean =>
|
||||
thumbnail.startsWith("https://brands.home-assistant.io/");
|
||||
|
||||
Reference in New Issue
Block a user