mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 01:06:35 +00:00
Merge pull request #12835 from home-assistant/dev
This commit is contained in:
commit
97f082a384
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@ -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
9
cast/public/_redirects
Normal 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
|
@ -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"
|
||||||
|
@ -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"] {
|
||||||
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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;
|
||||||
|
@ -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 || {};
|
||||||
|
@ -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%;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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")) {
|
||||||
|
@ -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}`,
|
||||||
|
@ -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 {
|
||||||
|
@ -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 });
|
||||||
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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" />
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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!",
|
||||||
|
@ -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/");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user