Merge pull request #12835 from home-assistant/dev

This commit is contained in:
Bram Kragten 2022-05-31 16:09:03 +02:00 committed by GitHub
commit 97f082a384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 579 additions and 254 deletions

2
.vscode/tasks.json vendored
View File

@ -181,7 +181,7 @@
{ {
"label": "Run HA Core for Supervisor in devcontainer", "label": "Run HA Core for Supervisor in devcontainer",
"type": "shell", "type": "shell",
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core", "command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
"isBackground": true, "isBackground": true,
"group": { "group": {
"kind": "build", "kind": "build",

9
cast/public/_redirects Normal file
View File

@ -0,0 +1,9 @@
# These redirects are handled by Netlify
#
# Some custom cards are not prefixing the instance URL when fetching data
# and can end up fetching the data from the Cast domain instead of HA.
# This will make sure that some common ones are replaced with a placeholder.
/api/camera_proxy/* /images/google-nest-hub.png
/api/camera_proxy_stream/* /images/google-nest-hub.png
/api/media_player_proxy/* /images/google-nest-hub.png

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20220526.0" version = "20220531.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -70,7 +70,9 @@ export const iconColorCSS = css`
} }
} }
ha-state-icon[data-domain="plant"][data-state="problem"], ha-state-icon[data-domain="plant"][data-state="problem"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] { ha-state-icon[data-state="unavailable"] {

View File

@ -1,3 +1,4 @@
import { LitElement } from "lit";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export function computeRTL(hass: HomeAssistant) { export function computeRTL(hass: HomeAssistant) {
@ -15,3 +16,21 @@ export function computeRTLDirection(hass: HomeAssistant) {
export function emitRTLDirection(rtl: boolean) { export function emitRTLDirection(rtl: boolean) {
return rtl ? "rtl" : "ltr"; 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"
);
}

View File

@ -37,6 +37,26 @@ export default class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set(); @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() { protected firstUpdated() {
this._setupChart(); this._setupChart();
this.data.datasets.forEach((dataset, index) => { this.data.datasets.forEach((dataset, index) => {

View File

@ -28,11 +28,11 @@ class StateHistoryChartLine extends LitElement {
@property({ type: Boolean }) public isSingleDevice = false; @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 _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions<"line">; @state() private _chartOptions?: ChartOptions;
protected render() { protected render() {
return html` return html`
@ -57,6 +57,7 @@ class StateHistoryChartLine extends LitElement {
locale: this.hass.locale, locale: this.hass.locale,
}, },
}, },
suggestedMax: this.endTime,
ticks: { ticks: {
maxRotation: 0, maxRotation: 0,
sampleSize: 5, sampleSize: 5,
@ -130,28 +131,11 @@ class StateHistoryChartLine extends LitElement {
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const entityStates = this.data; const entityStates = this.data;
const datasets: ChartDataset<"line">[] = []; const datasets: ChartDataset<"line">[] = [];
let endTime: Date;
if (entityStates.length === 0) { if (entityStates.length === 0) {
return; return;
} }
endTime = const endTime = this.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 || {}; const names = this.names || {};
entityStates.forEach((states) => { entityStates.forEach((states) => {
const domain = states.domain; const domain = states.domain;

View File

@ -83,6 +83,8 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public data: TimelineEntity[] = []; @property({ attribute: false }) public data: TimelineEntity[] = [];
@property() public narrow!: boolean;
@property() public names: boolean | Record<string, string> = false; @property() public names: boolean | Record<string, string> = false;
@property() public unit?: string; @property() public unit?: string;
@ -91,7 +93,11 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ type: Boolean }) public isSingleDevice = false; @property({ type: Boolean }) public isSingleDevice = false;
@property({ attribute: false }) public endTime?: Date; @property({ type: Boolean }) public dataHasMultipleRows = false;
@property({ attribute: false }) public startTime!: Date;
@property({ attribute: false }) public endTime!: Date;
@state() private _chartData?: ChartData<"timeline">; @state() private _chartData?: ChartData<"timeline">;
@ -110,6 +116,8 @@ export class StateHistoryChartTimeline extends LitElement {
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) { if (!this.hasUpdated) {
const narrow = this.narrow;
const multipleRows = this.data.length !== 1 || this.dataHasMultipleRows;
this._chartOptions = { this._chartOptions = {
maintainAspectRatio: false, maintainAspectRatio: false,
parsing: false, parsing: false,
@ -123,6 +131,8 @@ export class StateHistoryChartTimeline extends LitElement {
locale: this.hass.locale, locale: this.hass.locale,
}, },
}, },
suggestedMin: this.startTime,
suggestedMax: this.endTime,
ticks: { ticks: {
autoSkip: true, autoSkip: true,
maxRotation: 0, maxRotation: 0,
@ -153,11 +163,17 @@ export class StateHistoryChartTimeline extends LitElement {
drawTicks: false, drawTicks: false,
}, },
ticks: { ticks: {
display: this.data.length !== 1, display: multipleRows,
}, },
afterSetDimensions: (y) => { afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18; y.maxWidth = y.chart.width * 0.18;
}, },
afterFit: function (scaleInstance) {
if (multipleRows) {
// ensure all the chart labels are the same width
scaleInstance.width = narrow ? 105 : 185;
}
},
position: computeRTL(this.hass) ? "right" : "left", position: computeRTL(this.hass) ? "right" : "left",
}, },
}, },
@ -208,34 +224,8 @@ export class StateHistoryChartTimeline extends LitElement {
stateHistory = []; stateHistory = [];
} }
const startTime = new Date( const startTime = this.startTime;
stateHistory.reduce( const endTime = this.endTime;
(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 labels: string[] = [];
const datasets: ChartDataset<"timeline">[] = []; const datasets: ChartDataset<"timeline">[] = [];
const names = this.names || {}; const names = this.names || {};

View File

@ -1,3 +1,4 @@
import "@lit-labs/virtualizer";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -6,12 +7,29 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state, eventOptions } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HistoryResult } from "../../data/history"; import {
HistoryResult,
LineChartUnit,
TimelineEntity,
} from "../../data/history";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "./state-history-chart-line"; import "./state-history-chart-line";
import "./state-history-chart-timeline"; 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") @customElement("state-history-charts")
class StateHistoryCharts extends LitElement { class StateHistoryCharts extends LitElement {
@ -19,8 +37,13 @@ class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public historyData!: HistoryResult; @property({ attribute: false }) public historyData!: HistoryResult;
@property() public narrow!: boolean;
@property({ type: Boolean }) public names = false; @property({ type: Boolean }) public names = false;
@property({ type: Boolean, attribute: "virtualize", reflect: true })
public virtualize = false;
@property({ attribute: false }) public endTime?: Date; @property({ attribute: false }) public endTime?: Date;
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@ -29,6 +52,14 @@ class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public isLoadingData = false; @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 { protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) { if (!isComponentLoaded(this.hass, "history")) {
return html` <div class="info"> return html` <div class="info">
@ -48,40 +79,76 @@ class StateHistoryCharts extends LitElement {
</div>`; </div>`;
} }
const computedEndTime = this.upToNow const now = new Date();
? new Date()
: this.endTime || new Date();
return html` this._computedEndTime =
${this.historyData.timeline.length this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
? html`
<state-history-chart-timeline this._computedStartTime = new Date(
.hass=${this.hass} this.historyData.timeline.reduce(
.data=${this.historyData.timeline} (minTime, stateInfo) =>
.endTime=${computedEndTime} Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
.noSingle=${this.noSingle} new Date().getTime()
.names=${this.names} )
></state-history-chart-timeline> );
`
: html``} const combinedItems = chunkData(
${this.historyData.line.map( this.historyData.timeline,
(line) => html` CANVAS_TIMELINE_ROWS_CHUNK
<state-history-chart-line ).concat(this.historyData.line);
.hass=${this.hass}
.unit=${line.unit} return this.virtualize
.data=${line.data} ? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
.identifier=${line.identifier} <lit-virtualizer
.isSingleDevice=${!this.noSingle && scroller
line.data && class="ha-scrollbar"
line.data.length === 1} .items=${combinedItems}
.endTime=${computedEndTime} .renderItem=${this._renderHistoryItem}
.names=${this.names} >
></state-history-chart-line> </lit-virtualizer>
` </div>`
)} : html`${combinedItems.map((item, index) =>
`; this._renderHistoryItem(item, index)
)}`;
} }
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 &&
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}
.noSingle=${this.noSingle}
.names=${this.names}
.narrow=${this.narrow}
.dataHasMultipleRows=${this.historyData.timeline.length &&
this.historyData.timeline.length > 1}
></state-history-chart-timeline>
</div> `;
};
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass")); return !(changedProps.size === 1 && changedProps.has("hass"));
} }
@ -96,6 +163,11 @@ class StateHistoryCharts extends LitElement {
return !this.isLoadingData && historyDataEmpty; return !this.isLoadingData && historyDataEmpty;
} }
@eventOptions({ passive: true })
private _saveScrollPos(e: Event) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@ -103,11 +175,47 @@ class StateHistoryCharts extends LitElement {
/* height of single timeline chart = 60px */ /* height of single timeline chart = 60px */
min-height: 60px; min-height: 60px;
} }
:host([virtualize]) {
height: 100%;
}
.info { .info {
text-align: center; text-align: center;
line-height: 60px; line-height: 60px;
color: var(--secondary-text-color); 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%;
}
`; `;
} }
} }

View File

@ -1,13 +1,17 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light"; import type {
ComboBoxLight,
ComboBoxLightFilterChangedEvent,
ComboBoxLightOpenedChangedEvent,
ComboBoxLightValueChangedEvent,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield"; import "./ha-textfield";
@ -203,7 +207,7 @@ export class HaComboBox extends LitElement {
} }
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>) { private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value; const opened = ev.detail.value;
// delay this so we can handle click event before setting _opened // delay this so we can handle click event before setting _opened
setTimeout(() => { setTimeout(() => {
@ -229,14 +233,12 @@ export class HaComboBox extends LitElement {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if ( if (
mutation.type === "attributes" && mutation.type === "attributes" &&
mutation.attributeName === "inert" && mutation.attributeName === "inert"
// @ts-expect-error
overlay.inert === true
) { ) {
// @ts-expect-error
overlay.inert = false;
this._overlayMutationObserver?.disconnect(); this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined; this._overlayMutationObserver = undefined;
// @ts-expect-error
overlay.inert = false;
} else if (mutation.type === "childList") { } else if (mutation.type === "childList") {
mutation.removedNodes.forEach((node) => { mutation.removedNodes.forEach((node) => {
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") { if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
@ -257,12 +259,12 @@ export class HaComboBox extends LitElement {
} }
} }
private _filterChanged(ev: PolymerChangedEvent<string>) { private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail, { composed: false }); fireEvent(this, ev.type, ev.detail, { composed: false });
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;

View File

@ -103,6 +103,9 @@ export class HaTextSelector extends LitElement {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 16px;
direction: var(--direction);
} }
`; `;
} }

View File

@ -100,6 +100,7 @@ export class HaTextField extends TextFieldBase {
inset-inline-end: initial !important; inset-inline-end: initial !important;
transform-origin: var(--float-start); transform-origin: var(--float-start);
direction: var(--direction); direction: var(--direction);
transform-origin: var(--float-start);
} }
.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-text-field--with-leading-icon.mdc-text-field--filled

View File

@ -43,7 +43,11 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; import {
brandsUrl,
extractDomainFromBrandUrl,
isBrandUrl,
} from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
import "../ha-alert"; import "../ha-alert";
@ -563,6 +567,8 @@ export class HaMediaPlayerBrowse extends LitElement {
<div <div
class="${["app", "directory"].includes(child.media_class) class="${["app", "directory"].includes(child.media_class)
? "centered-image" ? "centered-image"
: ""} ${isBrandUrl(child.thumbnail)
? "brand-image"
: ""} image" : ""} image"
style="background-image: ${until(backgroundImage, "")}" style="background-image: ${until(backgroundImage, "")}"
></div> ></div>
@ -661,7 +667,7 @@ export class HaMediaPlayerBrowse extends LitElement {
return (await getSignedPath(this.hass, thumbnailUrl)).path; return (await getSignedPath(this.hass, thumbnailUrl)).path;
} }
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) { if (isBrandUrl(thumbnailUrl)) {
// The backend is not aware of the theme used by the users, // The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon // so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({ thumbnailUrl = brandsUrl({
@ -1050,6 +1056,10 @@ export class HaMediaPlayerBrowse extends LitElement {
background-size: contain; background-size: contain;
} }
.brand-image {
background-size: 40%;
}
.children ha-card .icon-holder { .children ha-card .icon-holder {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -240,6 +240,7 @@ export interface EnergyData {
prefs: EnergyPreferences; prefs: EnergyPreferences;
info: EnergyInfo; info: EnergyInfo;
stats: Statistics; stats: Statistics;
statsMetadata: Record<string, StatisticsMetaData>;
statsCompare: Statistics; statsCompare: Statistics;
co2SignalConfigEntry?: ConfigEntry; co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string; co2SignalEntity?: string;
@ -285,15 +286,6 @@ const getEnergyData = async (
const consumptionStatIDs: string[] = []; const consumptionStatIDs: string[] = [];
const statIDs: 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) { for (const source of prefs.energy_sources) {
if (source.type === "solar") { if (source.type === "solar") {
@ -303,20 +295,6 @@ const getEnergyData = async (
if (source.type === "gas") { if (source.type === "gas") {
statIDs.push(source.stat_energy_from); 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) { if (source.stat_cost) {
statIDs.push(source.stat_cost); statIDs.push(source.stat_cost);
} }
@ -432,6 +410,12 @@ 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 = { const data: EnergyData = {
start, start,
end, end,
@ -440,6 +424,7 @@ const getEnergyData = async (
info, info,
prefs, prefs,
stats, stats,
statsMetadata,
statsCompare, statsCompare,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
@ -628,13 +613,13 @@ export const getEnergyGasUnitCategory = (
export const getEnergyGasUnit = ( export const getEnergyGasUnit = (
hass: HomeAssistant, hass: HomeAssistant,
prefs: EnergyPreferences prefs: EnergyPreferences,
statisticsMetaData: Record<string, StatisticsMetaData> = {}
): string | undefined => { ): string | undefined => {
for (const source of prefs.energy_sources) { for (const source of prefs.energy_sources) {
if (source.type !== "gas") { if (source.type !== "gas") {
continue; continue;
} }
const entity = hass.states[source.stat_energy_from]; const entity = hass.states[source.stat_energy_from];
if (entity?.attributes.unit_of_measurement) { if (entity?.attributes.unit_of_measurement) {
// Wh is normalized to kWh by stats generation // Wh is normalized to kWh by stats generation
@ -642,8 +627,11 @@ export const getEnergyGasUnit = (
? "kWh" ? "kWh"
: entity.attributes.unit_of_measurement; : entity.attributes.unit_of_measurement;
} }
if (source.unit_of_measurement) { const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
return source.unit_of_measurement; if (statisticIdWithMeta?.unit_of_measurement) {
return statisticIdWithMeta.unit_of_measurement === "Wh"
? "kWh"
: statisticIdWithMeta.unit_of_measurement;
} }
} }
return undefined; return undefined;

View File

@ -1,7 +1,10 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import {
computeStateName,
computeStateNameFromEntityAttributes,
} from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation"; import { FrontendLocaleData } from "./translation";
@ -547,3 +550,16 @@ export const adjustStatisticsSum = (
start_time, start_time,
adjustment, 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;
};

View File

@ -78,6 +78,7 @@ class MoreInfoMediaPlayer extends LitElement {
@click=${this._showBrowseMedia} @click=${this._showBrowseMedia}
> >
<ha-svg-icon <ha-svg-icon
class="browse-media-icon"
.path=${mdiPlayBoxMultiple} .path=${mdiPlayBoxMultiple}
slot="icon" slot="icon"
></ha-svg-icon> ></ha-svg-icon>
@ -243,6 +244,10 @@ class MoreInfoMediaPlayer extends LitElement {
mwc-button > ha-svg-icon { mwc-button > ha-svg-icon {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.browse-media-icon {
margin-left: 8px;
}
`; `;
} }

View File

@ -51,11 +51,15 @@ function initialize(
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = ` style.innerHTML = `
body { margin:0; } body {
margin:0;
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
background-color: #111111; background-color: var(--primary-background-color, #111111);
color: #e1e1e1; color: var(--primary-text-color, #e1e1e1);
} }
}`; }`;
document.head.appendChild(style); document.head.appendChild(style);

View File

@ -3,6 +3,8 @@ import { property } from "lit/decorators";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize"; import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types"; import { Constructor, Resources } from "../types";
import { getLocalLanguage, getTranslation } from "../util/common-translation"; import { getLocalLanguage, getTranslation } from "../util/common-translation";
import { translationMetadata } from "../resources/translations-metadata";
import { computeDirectionStyles } from "../common/util/compute_rtl";
const empty = () => ""; const empty = () => "";
@ -25,6 +27,14 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
this._initializeLocalizeLite(); this._initializeLocalizeLite();
} }
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
computeDirectionStyles(
translationMetadata.translations[this.language!].isRTL,
this
);
}
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.get("translationFragment")) { if (changedProperties.get("translationFragment")) {

View File

@ -53,7 +53,6 @@ export class HaConfigApplicationCredentials extends LitElement {
title: localize( title: localize(
"ui.panel.config.application_credentials.picker.headers.name" "ui.panel.config.application_credentials.picker.headers.name"
), ),
width: "40%",
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (_, entry: ApplicationCredential) => html`${entry.name}`, template: (_, entry: ApplicationCredential) => html`${entry.name}`,

View File

@ -315,7 +315,8 @@ export class HaBlueprintAutomationEditor extends LitElement {
padding: 0 16px 16px; padding: 0 16px 16px;
} }
ha-textarea, ha-textarea,
ha-textfield { ha-textfield,
ha-blueprint-picker {
display: block; display: block;
} }
h3 { h3 {

View File

@ -106,7 +106,9 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
const offsetType = ev.detail.value.offset_type === "before" ? "-" : ""; const offsetType = ev.detail.value.offset_type === "before" ? "-" : "";
const newTrigger = { const newTrigger = {
...ev.detail.value, ...ev.detail.value,
offset: `${offsetType}${duration.hours}:${duration.minutes}:${duration.seconds}`, offset: `${offsetType}${duration.hours ?? 0}:${duration.minutes ?? 0}:${
duration.seconds ?? 0
}`,
}; };
delete newTrigger.offset_type; delete newTrigger.offset_type;
fireEvent(this, "value-changed", { value: newTrigger }); fireEvent(this, "value-changed", { value: newTrigger });

View File

@ -125,6 +125,7 @@ class HaConfigHardware extends LitElement {
<div class="card-content"> <div class="card-content">
<mwc-list> <mwc-list>
<mwc-list-item <mwc-list-item
noninteractive
graphic=${ifDefined(imageURL ? "medium" : undefined)} graphic=${ifDefined(imageURL ? "medium" : undefined)}
.twoline=${Boolean(boardId)} .twoline=${Boolean(boardId)}
> >

View File

@ -694,6 +694,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
justify-content: space-between; justify-content: space-between;
} }
span[slot="meta"] {
font-size: 0.95em;
color: var(--primary-text-color);
}
.network-status div.heading { .network-status div.heading {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -80,44 +80,44 @@ class HaPanelHistory extends LitElement {
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="flex content"> <div class="filters">
<div class="filters"> <ha-date-range-picker
<ha-date-range-picker .hass=${this.hass}
.hass=${this.hass} ?disabled=${this._isLoading}
?disabled=${this._isLoading} .startDate=${this._startDate}
.startDate=${this._startDate} .endDate=${this._endDate}
.endDate=${this._endDate} .ranges=${this._ranges}
.ranges=${this._ranges} @change=${this._dateRangeChanged}
@change=${this._dateRangeChanged} ></ha-date-range-picker>
></ha-date-range-picker>
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._entityId} .value=${this._entityId}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.entity" "ui.components.entity.entity-picker.entity"
)} )}
.disabled=${this._isLoading} .disabled=${this._isLoading}
@change=${this._entityPicked} @change=${this._entityPicked}
></ha-entity-picker> ></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> </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> </ha-app-layout>
`; `;
} }
@ -235,6 +235,14 @@ class HaPanelHistory extends LitElement {
padding: 0 16px 16px; padding: 0 16px 16px;
} }
state-history-charts {
height: calc(100vh - 136px);
}
:host([narrow]) state-history-charts {
height: calc(100vh - 198px);
}
.progress-wrapper { .progress-wrapper {
height: calc(100vh - 136px); height: calc(100vh - 136px);
} }
@ -243,6 +251,10 @@ class HaPanelHistory extends LitElement {
height: calc(100vh - 198px); height: calc(100vh - 198px);
} }
:host([virtualize]) {
height: 100%;
}
.progress-wrapper { .progress-wrapper {
position: relative; position: relative;
} }

View File

@ -310,7 +310,7 @@ export class HaLogbook extends LitElement {
// Put newest ones on top. Reverse works in-place so // Put newest ones on top. Reverse works in-place so
// make a copy first. // make a copy first.
const newEntries = [...streamMessage.events].reverse(); const newEntries = [...streamMessage.events].reverse();
if (!this._logbookEntries) { if (!this._logbookEntries || !this._logbookEntries.length) {
this._logbookEntries = newEntries; this._logbookEntries = newEntries;
return; return;
} }
@ -320,14 +320,16 @@ export class HaLogbook extends LitElement {
return; return;
} }
const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime); const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime);
this._logbookEntries = this._logbookEntries = !nonExpiredRecords.length
newEntries[0].when >= this._logbookEntries[0].when ? // All existing entries expired
? // The new records are newer than the old records newEntries
// append the old records to the end of the new records : newEntries[0].when >= nonExpiredRecords[0].when
newEntries.concat(nonExpiredRecords) ? // The new records are newer than the old records
: // The new records are older than the old records // append the old records to the end of the new records
// append the new records to the end of the old records newEntries.concat(nonExpiredRecords)
nonExpiredRecords.concat(newEntries); : // 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 () => { private _updateTraceContexts = throttle(async () => {

View File

@ -43,8 +43,6 @@ export class HuiEnergyDevicesGraphCard
@state() private _config?: EnergyDevicesGraphCardConfig; @state() private _config?: EnergyDevicesGraphCardConfig;
@state() private _data?: Statistics;
@state() private _chartData: ChartData = { datasets: [] }; @state() private _chartData: ChartData = { datasets: [] };
@query("ha-chart-base") private _chart?: HaChartBase; @query("ha-chart-base") private _chart?: HaChartBase;
@ -162,19 +160,24 @@ export class HuiEnergyDevicesGraphCard
energyData.start energyData.start
); );
this._data = await fetchStatistics( const devices = energyData.prefs.device_consumption.map(
this.hass, (device) => device.stat_consumption
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 startMinHour = addHours(energyData.start, -1);
Object.values(this._data).forEach((stat) => { const data = await fetchStatistics(
this.hass,
startMinHour,
energyData.end,
devices,
period
);
Object.values(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 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) { if (stat.length && new Date(stat[0].start) > startMinHour) {
stat.unshift({ stat.unshift({
@ -187,9 +190,41 @@ export class HuiEnergyDevicesGraphCard
} }
}); });
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = []; 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 borderColor: string[] = []; const borderColor: string[] = [];
const borderColorCompare: string[] = [];
const backgroundColor: string[] = []; const backgroundColor: string[] = [];
const backgroundColorCompare: string[] = [];
const datasets: ChartDataset<"bar", ParsedDataType<"bar">[]>[] = [ const datasets: ChartDataset<"bar", ParsedDataType<"bar">[]>[] = [
{ {
@ -198,35 +233,69 @@ export class HuiEnergyDevicesGraphCard
), ),
borderColor, borderColor,
backgroundColor, backgroundColor,
data, data: chartData,
barThickness: 20, barThickness: compareData ? 10 : 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) => { energyData.prefs.device_consumption.forEach((device, idx) => {
const value = const value =
device.stat_consumption in this._data! device.stat_consumption in data
? calculateStatisticSumGrowth(this._data![device.stat_consumption]) || ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
0
: 0; : 0;
data.push({ chartData.push({
// @ts-expect-error // @ts-expect-error
y: device.stat_consumption, y: device.stat_consumption,
x: value, x: value,
idx, 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,
});
}
}); });
data.sort((a, b) => b.x - a.x); chartData.sort((a, b) => b.x - a.x);
data.forEach((d: any) => { chartData.forEach((d: any) => {
const color = getColorByIndex(d.idx); const color = getColorByIndex(d.idx);
borderColor.push(color); borderColor.push(color);
backgroundColor.push(color + "7F"); backgroundColor.push(color + "7F");
}); });
chartDataCompare.forEach((d: any) => {
const color = getColorByIndex(d.idx);
borderColorCompare.push(color + "7F");
backgroundColorCompare.push(color + "32");
});
this._chartData = { this._chartData = {
datasets, datasets,
}; };

View File

@ -315,7 +315,11 @@ class HuiEnergyDistrubutionCard
${formatNumber(gasUsage || 0, this.hass.locale, { ${formatNumber(gasUsage || 0, this.hass.locale, {
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
${getEnergyGasUnit(this.hass, prefs) || "m³"} ${getEnergyGasUnit(
this.hass,
prefs,
this._data.statsMetadata
) || "m³"}
</div> </div>
<svg width="80" height="30"> <svg width="80" height="30">
<path d="M40 0 v30" id="gas" /> <path d="M40 0 v30" id="gas" />

View File

@ -26,7 +26,6 @@ import {
import { labBrighten, labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date"; import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
@ -39,7 +38,11 @@ import {
getEnergyDataCollection, getEnergyDataCollection,
getEnergyGasUnit, getEnergyGasUnit,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { Statistics } from "../../../../data/history"; import {
Statistics,
StatisticsMetaData,
getStatisticLabel,
} from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -270,7 +273,9 @@ export class HuiEnergyGasGraphCard
(source) => source.type === "gas" (source) => source.type === "gas"
) as GasSourceTypeEnergyPreference[]; ) as GasSourceTypeEnergyPreference[];
this._unit = getEnergyGasUnit(this.hass, energyData.prefs) || "m³"; this._unit =
getEnergyGasUnit(this.hass, energyData.prefs, energyData.statsMetadata) ||
"m³";
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = []; const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
@ -280,7 +285,12 @@ export class HuiEnergyGasGraphCard
.trim(); .trim();
datasets.push( datasets.push(
...this._processDataSet(energyData.stats, gasSources, gasColor) ...this._processDataSet(
energyData.stats,
energyData.statsMetadata,
gasSources,
gasColor
)
); );
if (energyData.statsCompare) { if (energyData.statsCompare) {
@ -298,6 +308,7 @@ export class HuiEnergyGasGraphCard
datasets.push( datasets.push(
...this._processDataSet( ...this._processDataSet(
energyData.statsCompare, energyData.statsCompare,
energyData.statsMetadata,
gasSources, gasSources,
gasColor, gasColor,
true true
@ -318,14 +329,14 @@ export class HuiEnergyGasGraphCard
private _processDataSet( private _processDataSet(
statistics: Statistics, statistics: Statistics,
statisticsMetaData: Record<string, StatisticsMetaData>,
gasSources: GasSourceTypeEnergyPreference[], gasSources: GasSourceTypeEnergyPreference[],
gasColor: string, gasColor: string,
compare = false compare = false
) { ) {
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = []; const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
gasSources.forEach((source, idx) => {
const entity = this.hass.states[source.stat_energy_from];
gasSources.forEach((source, idx) => {
const modifiedColor = const modifiedColor =
idx > 0 idx > 0
? this.hass.themes.darkMode ? this.hass.themes.darkMode
@ -368,7 +379,11 @@ export class HuiEnergyGasGraphCard
} }
data.push({ data.push({
label: entity ? computeStateName(entity) : source.stat_energy_from, label: getStatisticLabel(
this.hass,
source.stat_energy_from,
statisticsMetaData
),
borderColor: compare ? borderColor + "7F" : borderColor, borderColor: compare ? borderColor + "7F" : borderColor,
backgroundColor: compare ? borderColor + "32" : borderColor + "7F", backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
data: gasConsumptionData, data: gasConsumptionData,

View File

@ -40,7 +40,11 @@ import {
getEnergySolarForecasts, getEnergySolarForecasts,
SolarSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { Statistics } from "../../../../data/history"; import {
Statistics,
StatisticsMetaData,
getStatisticLabel,
} from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -289,7 +293,12 @@ export class HuiEnergySolarGraphCard
.trim(); .trim();
datasets.push( datasets.push(
...this._processDataSet(energyData.stats, solarSources, solarColor) ...this._processDataSet(
energyData.stats,
energyData.statsMetadata,
solarSources,
solarColor
)
); );
if (energyData.statsCompare) { if (energyData.statsCompare) {
@ -307,6 +316,7 @@ export class HuiEnergySolarGraphCard
datasets.push( datasets.push(
...this._processDataSet( ...this._processDataSet(
energyData.statsCompare, energyData.statsCompare,
energyData.statsMetadata,
solarSources, solarSources,
solarColor, solarColor,
true true
@ -339,6 +349,7 @@ export class HuiEnergySolarGraphCard
private _processDataSet( private _processDataSet(
statistics: Statistics, statistics: Statistics,
statisticsMetaData: Record<string, StatisticsMetaData>,
solarSources: SolarSourceTypeEnergyPreference[], solarSources: SolarSourceTypeEnergyPreference[],
solarColor: string, solarColor: string,
compare = false compare = false
@ -346,8 +357,6 @@ export class HuiEnergySolarGraphCard
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = []; const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
solarSources.forEach((source, idx) => { solarSources.forEach((source, idx) => {
const entity = this.hass.states[source.stat_energy_from];
const modifiedColor = const modifiedColor =
idx > 0 idx > 0
? this.hass.themes.darkMode ? this.hass.themes.darkMode
@ -393,7 +402,11 @@ export class HuiEnergySolarGraphCard
label: this.hass.localize( label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_solar_graph.production", "ui.panel.lovelace.cards.energy.energy_solar_graph.production",
{ {
name: entity ? computeStateName(entity) : source.stat_energy_from, name: getStatisticLabel(
this.hass,
source.stat_energy_from,
statisticsMetaData
),
} }
), ),
borderColor: compare ? borderColor + "7F" : borderColor, borderColor: compare ? borderColor + "7F" : borderColor,

View File

@ -128,7 +128,9 @@ export class HuiEnergySourcesTableCard
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
); );
const gasUnit = getEnergyGasUnit(this.hass, this._data.prefs) || ""; const gasUnit =
getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
"";
const compare = this._data.statsCompare !== undefined; const compare = this._data.statsCompare !== undefined;
@ -849,7 +851,9 @@ export class HuiEnergySourcesTableCard
)} )}
</th> </th>
${compare ${compare
? html`<td class="mdc-data-table__cell"> ? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber( ${formatNumber(
totalGasCostCompare + totalGridCostCompare, totalGasCostCompare + totalGridCostCompare,
this.hass.locale, this.hass.locale,
@ -860,9 +864,7 @@ export class HuiEnergySourcesTableCard
)} )}
</td> </td>
${showCosts ${showCosts
? html`<td ? html`<td class="mdc-data-table__cell"></td>`
class="mdc-data-table__cell mdc-data-table__cell--numeric"
></td>`
: ""}` : ""}`
: ""} : ""}
<td class="mdc-data-table__cell"></td> <td class="mdc-data-table__cell"></td>

View File

@ -26,7 +26,6 @@ import {
import { labBrighten, labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date"; import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
@ -34,7 +33,11 @@ import {
import "../../../../components/chart/ha-chart-base"; import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import { Statistics } from "../../../../data/history"; import {
Statistics,
StatisticsMetaData,
getStatisticLabel,
} from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -378,7 +381,14 @@ export class HuiEnergyUsageGraphCard
this._compareEnd = energyData.endCompare; this._compareEnd = energyData.endCompare;
datasets.push( datasets.push(
...this._processDataSet(energyData.stats, statIds, colors, labels, false) ...this._processDataSet(
energyData.stats,
energyData.statsMetadata,
statIds,
colors,
labels,
false
)
); );
if (energyData.statsCompare) { if (energyData.statsCompare) {
@ -396,6 +406,7 @@ export class HuiEnergyUsageGraphCard
datasets.push( datasets.push(
...this._processDataSet( ...this._processDataSet(
energyData.statsCompare, energyData.statsCompare,
energyData.statsMetadata,
statIds, statIds,
colors, colors,
labels, labels,
@ -411,6 +422,7 @@ export class HuiEnergyUsageGraphCard
private _processDataSet( private _processDataSet(
statistics: Statistics, statistics: Statistics,
statisticsMetaData: Record<string, StatisticsMetaData>,
statIdsByCat: { statIdsByCat: {
to_grid?: string[] | undefined; to_grid?: string[] | undefined;
from_grid?: string[] | undefined; from_grid?: string[] | undefined;
@ -580,8 +592,6 @@ export class HuiEnergyUsageGraphCard
Object.entries(combinedData).forEach(([type, sources]) => { Object.entries(combinedData).forEach(([type, sources]) => {
Object.entries(sources).forEach(([statId, source], idx) => { Object.entries(sources).forEach(([statId, source], idx) => {
const entity = this.hass.states[statId];
const modifiedColor = const modifiedColor =
idx > 0 idx > 0
? this.hass.themes.darkMode ? this.hass.themes.darkMode
@ -610,9 +620,7 @@ export class HuiEnergyUsageGraphCard
label: label:
type in labels type in labels
? labels[type] ? labels[type]
: entity : getStatisticLabel(this.hass, statId, statisticsMetaData),
? computeStateName(entity)
: statId,
order: order:
type === "used_solar" type === "used_solar"
? 1 ? 1

View File

@ -1,5 +1,10 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; import {
mdiChevronLeft,
mdiChevronRight,
mdiCompare,
mdiCompareRemove,
} from "@mdi/js";
import { import {
addDays, addDays,
addMonths, addMonths,
@ -40,13 +45,15 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@property() public collectionKey?: string; @property() public collectionKey?: string;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() _startDate?: Date; @state() _startDate?: Date;
@state() _endDate?: Date; @state() _endDate?: Date;
@state() private _period?: "day" | "week" | "month" | "year"; @state() private _period?: "day" | "week" | "month" | "year";
@state() private _compare? = false; @state() private _compare = false;
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@ -136,14 +143,24 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
dense dense
@value-changed=${this._handleView} @value-changed=${this._handleView}
></ha-button-toggle-group> ></ha-button-toggle-group>
<mwc-button ${this.narrow
class="compare ${this._compare ? "active" : ""}" ? html`<ha-icon-button
@click=${this._toggleCompare} class="compare ${this._compare ? "active" : ""}"
dense .path=${this._compare ? mdiCompareRemove : mdiCompare}
outlined @click=${this._toggleCompare}
> dense
Compare data outlined
</mwc-button> >
Compare data
</ha-icon-button>`
: html`<mwc-button
class="compare ${this._compare ? "active" : ""}"
@click=${this._toggleCompare}
dense
outlined
>
Compare data
</mwc-button>`}
</div> </div>
</div> </div>
`; `;
@ -260,9 +277,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
:host([narrow]) .row { :host([narrow]) .row {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
:host([narrow]) .period {
margin-bottom: 8px;
}
.label { .label {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -275,6 +289,17 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
justify-content: flex-end; justify-content: flex-end;
align-items: 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 { mwc-button.active::before {
top: 0; top: 0;
left: 0; left: 0;
@ -288,14 +313,11 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
transition: opacity 15ms linear, background-color 15ms linear; transition: opacity 15ms linear, background-color 15ms linear;
opacity: var(--mdc-icon-button-ripple-opacity, 0.12); opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
} }
ha-icon-button.active::before {
border-radius: 50%;
}
.compare { .compare {
position: relative; position: relative;
margin-left: 8px;
width: max-content;
}
:host([narrow]) .compare {
margin-left: auto;
margin-top: 8px;
} }
:host { :host {
--mdc-button-outline-color: currentColor; --mdc-button-outline-color: currentColor;

View File

@ -1,6 +1,9 @@
import { atLeastVersion } from "../common/config/version"; import { atLeastVersion } from "../common/config/version";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize"; import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { computeRTLDirection } from "../common/util/compute_rtl"; import {
computeRTLDirection,
setDirectionStyles,
} from "../common/util/compute_rtl";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { import {
getHassTranslations, getHassTranslations,
@ -188,17 +191,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
private _applyDirection(hass: HomeAssistant) { private _applyDirection(hass: HomeAssistant) {
const direction = computeRTLDirection(hass); const direction = computeRTLDirection(hass);
this.style.direction = direction;
document.dir = direction; document.dir = direction;
this.style.setProperty("--direction", direction); setDirectionStyles(direction, this);
this.style.setProperty(
"--float-start",
direction === "ltr" ? "left" : "right"
);
this.style.setProperty(
"--float-end",
direction === "ltr" ? "right" : "left"
);
} }
/** /**

View File

@ -2376,6 +2376,7 @@
"instance_is_available": "Your instance is available at your", "instance_is_available": "Your instance is available at your",
"instance_will_be_available": "Your instance will be available at your", "instance_will_be_available": "Your instance will be available at your",
"link_learn_how_it_works": "Learn how it works", "link_learn_how_it_works": "Learn how it works",
"nabu_casa_url": "Nabu Casa URL",
"certificate_info": "Certificate Info" "certificate_info": "Certificate Info"
}, },
"alexa": { "alexa": {
@ -3404,7 +3405,8 @@
"go_to_energy_dashboard": "Go to the energy dashboard" "go_to_energy_dashboard": "Go to the energy dashboard"
}, },
"energy_devices_graph": { "energy_devices_graph": {
"energy_usage": "Energy usage" "energy_usage": "Energy usage",
"previous_energy_usage": "Previous energy usage"
}, },
"carbon_consumed_gauge": { "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!", "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!",

View File

@ -23,3 +23,6 @@ export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`; }${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4]; export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4];
export const isBrandUrl = (thumbnail: string | ""): boolean =>
thumbnail.startsWith("https://brands.home-assistant.io/");