Compare commits

..

25 Commits

Author SHA1 Message Date
Paul Bottein
1228f17ffb Don't add audio track is webrtc player is muted 2025-06-12 14:58:06 +02:00
Paul Bottein
01d2ef13c6 Ensure grid options always return an object (#25760) 2025-06-12 10:44:20 +03:00
Petar Petrov
af6911e848 Fix alerts refresh on device page (#25748)
* Fix alerts refresh on device page

* don't reset actions periodically

* reset stuff only on deviceId change
2025-06-12 09:40:36 +02:00
Paul Bottein
21af10fd28 Change backup type order (#25759) 2025-06-12 07:29:58 +00:00
Paul Bottein
6d30d15638 Fix edit card not working in chrome after editing (#25751) 2025-06-11 14:15:50 +00:00
Paul Bottein
d542b52ebd Display full error for card preview mode (#25747) 2025-06-11 13:04:54 +03:00
c0ffeeca7
43cc49bb32 Z-Wave: apply sentence-style capitalization (#25739) 2025-06-11 09:13:32 +03:00
karwosts
b3f0a6328e More support for no-grid energy dashboard (#25644)
* More support for no-grid energy dashboard

* Update src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* lint

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-06-11 09:11:46 +03:00
Paulus Schoutsen
9d6a7e7e6f Assist Chat: handle intent progress delta not always being there (#25730)
handle intent progress data type change
2025-06-10 10:59:10 -04:00
Paul Bottein
78d7da21aa Fix custom value selected when clicking item in combo box (#25734) 2025-06-10 16:46:09 +02:00
Paul Bottein
0474a24df6 Allow to open more info using query params (#25733) 2025-06-10 13:20:10 +03:00
Paul Bottein
6e7ac6fdf7 Reduce keypad gap and margin in alarm panel card (#25735) 2025-06-10 13:16:06 +03:00
Petar Petrov
7b9683df89 Fix period boundaries in Energy dashboard (#25728) 2025-06-10 10:28:51 +02:00
renovate[bot]
8523ddfd29 Update vaadinWebComponents monorepo to v24.7.8 (#25729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 15:50:36 +03:00
Petar Petrov
2589e1a49f Handle tiny values in a log chart (#25727) 2025-06-09 14:45:23 +03:00
Paul Schreiber
5ce5f9a189 fix spelling of JavaScript in bug report template (#25726)
Correctly capitalize JavaScript in the bug report template.
2025-06-09 06:08:58 +00:00
renovate[bot]
6dd7217a20 Update dependency @babel/runtime to v7.27.6 (#25722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 09:01:08 +03:00
renovate[bot]
0d02d0d334 Update vitest monorepo to v3.2.2 (#25723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 09:00:45 +03:00
renovate[bot]
fed0dfa091 Update vitest monorepo to v3.2.1 (#25715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 09:09:17 +02:00
renovate[bot]
39de40dec9 Update Yarn to v4.9.2 (#25714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 09:09:11 +02:00
renovate[bot]
e1c42d9985 Update dependency typescript-eslint to v8.33.1 (#25712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:33:20 +02:00
renovate[bot]
ad375c9b01 Update dependency hls.js to v1.6.5 (#25711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:33:01 +02:00
renovate[bot]
07230e5ef5 Update dependency @codemirror/language to v6.11.1 (#25708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:59:35 +02:00
renovate[bot]
52f5af6090 Update dependency @rsdoctor/rspack-plugin to v1.1.3 (#25709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:59:08 +02:00
karwosts
3c07289077 Handle shorthand template conditions in trace (#25705) 2025-06-06 18:10:10 +02:00
45 changed files with 1074 additions and 976 deletions

View File

@@ -108,9 +108,9 @@ body:
render: yaml render: yaml
- type: textarea - type: textarea
attributes: attributes:
label: Javascript errors shown in your browser console/inspector label: JavaScript errors shown in your browser console/inspector
description: > description: >
If you come across any Javascript or other error logs, e.g., in your If you come across any JavaScript or other error logs, e.g., in your
browser console/inspector please provide them. browser console/inspector please provide them.
render: txt render: txt
- type: textarea - type: textarea

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.1.cjs yarnPath: .yarn/releases/yarn-4.9.2.cjs

View File

@@ -26,11 +26,11 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.27.4", "@babel/runtime": "7.27.6",
"@braintree/sanitize-url": "7.1.1", "@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6", "@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1", "@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.0", "@codemirror/language": "6.11.1",
"@codemirror/legacy-modes": "6.5.1", "@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.11", "@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2", "@codemirror/state": "6.5.2",
@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1", "@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0", "@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.7", "@vaadin/combo-box": "24.7.8",
"@vaadin/vaadin-themable-mixin": "24.7.7", "@vaadin/vaadin-themable-mixin": "24.7.8",
"@vibrant/color": "4.0.0", "@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0", "@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10", "@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -111,7 +111,7 @@
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2", "gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.4", "hls.js": "1.6.5",
"home-assistant-js-websocket": "9.5.0", "home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2", "idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16", "intl-messageformat": "10.7.16",
@@ -158,7 +158,7 @@
"@octokit/auth-oauth-device": "8.0.1", "@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "8.0.1", "@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.1.2", "@rsdoctor/rspack-plugin": "1.1.3",
"@rspack/cli": "1.3.12", "@rspack/cli": "1.3.12",
"@rspack/core": "1.3.12", "@rspack/core": "1.3.12",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
@@ -179,7 +179,7 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.1.4", "@vitest/coverage-v8": "3.2.2",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
@@ -218,9 +218,9 @@
"terser-webpack-plugin": "5.3.14", "terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.8.3", "typescript": "5.8.3",
"typescript-eslint": "8.33.0", "typescript-eslint": "8.33.1",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.4", "vitest": "3.2.2",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -236,5 +236,5 @@
"tslib": "2.8.1", "tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
}, },
"packageManager": "yarn@4.9.1" "packageManager": "yarn@4.9.2"
} }

View File

@@ -229,14 +229,20 @@ export class StateHistoryChartLine extends LitElement {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!); minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
} }
} else if (this.logarithmicScale) { } else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); minYAxis = ({ min }) => {
const value = min > 0 ? min * 0.95 : min * 1.05;
return Math.abs(value) < 1 ? value : Math.floor(value);
};
} }
if (typeof maxYAxis === "number") { if (typeof maxYAxis === "number") {
if (this.fitYData) { if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
} }
} else if (this.logarithmicScale) { } else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); maxYAxis = ({ max }) => {
const value = max > 0 ? max * 1.05 : max * 0.95;
return Math.abs(value) < 1 ? value : Math.ceil(value);
};
} }
this._chartOptions = { this._chartOptions = {
xAxis: { xAxis: {
@@ -753,10 +759,10 @@ export class StateHistoryChartLine extends LitElement {
if (this.logarithmicScale) { if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value // log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") { if (typeof value === "number") {
return Math.max(value, 0.1); return Math.max(value, Number.EPSILON);
} }
if (typeof value === "function") { if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1); return (values: any) => Math.max(value(values), Number.EPSILON);
} }
} }
return value; return value;

View File

@@ -241,14 +241,20 @@ export class StatisticsChart extends LitElement {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!); minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
} }
} else if (this.logarithmicScale) { } else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05); minYAxis = ({ min }) => {
const value = min > 0 ? min * 0.95 : min * 1.05;
return Math.abs(value) < 1 ? value : Math.floor(value);
};
} }
if (typeof maxYAxis === "number") { if (typeof maxYAxis === "number") {
if (this.fitYData) { if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!); maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
} }
} else if (this.logarithmicScale) { } else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95); maxYAxis = ({ max }) => {
const value = max > 0 ? max * 1.05 : max * 0.95;
return Math.abs(value) < 1 ? value : Math.ceil(value);
};
} }
const endTime = this.endTime ?? new Date(); const endTime = this.endTime ?? new Date();
let startTime = this.startTime; let startTime = this.startTime;
@@ -619,10 +625,10 @@ export class StatisticsChart extends LitElement {
if (this.logarithmicScale) { if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value // log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") { if (typeof value === "number") {
return Math.max(value, 0.1); return Math.max(value, Number.EPSILON);
} }
if (typeof value === "function") { if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1); return (values: any) => Math.max(value(values), Number.EPSILON);
} }
} }
return value; return value;

View File

@@ -509,7 +509,7 @@ export class HaAssistChat extends LitElement {
this.requestUpdate("_conversation"); this.requestUpdate("_conversation");
}, },
processEvent: (event: PipelineRunEvent) => { processEvent: (event: PipelineRunEvent) => {
if (event.type === "intent-progress") { if (event.type === "intent-progress" && event.data.chat_log_delta) {
const delta = event.data.chat_log_delta; const delta = event.data.chat_log_delta;
// new message // new message

View File

@@ -220,9 +220,10 @@ export class HaCameraStream extends LitElement {
if ( if (
hlsStreams.hasVideo && hlsStreams.hasVideo &&
hlsStreams.hasAudio && hlsStreams.hasAudio &&
!webRtcStreams.hasAudio !webRtcStreams.hasAudio &&
!this.muted
) { ) {
// webRTC stream is missing audio, use HLS // webRTC stream is missing audio and video is not muted, use HLS
return [{ type: STREAM_TYPE_HLS, visible: true }]; return [{ type: STREAM_TYPE_HLS, visible: true }];
} }
if (webRtcStreams.hasVideo) { if (webRtcStreams.hasVideo) {

View File

@@ -345,8 +345,10 @@ export class HaComboBox extends LitElement {
// @ts-ignore // @ts-ignore
this._comboBox._closeOnBlurIsPrevented = true; this._comboBox._closeOnBlurIsPrevented = true;
} }
if (!this.opened) {
return;
}
const newValue = ev.detail.value; const newValue = ev.detail.value;
if (newValue !== this.value) { if (newValue !== this.value) {
fireEvent(this, "value-changed", { value: newValue || undefined }); fireEvent(this, "value-changed", { value: newValue || undefined });
} }

View File

@@ -44,7 +44,7 @@ const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`, path: `/${panel.url_path}`,
icon: panel.icon ?? "mdi:view-dashboard", icon: panel.icon ?? "mdi:view-dashboard",
title: title:
panel.url_path === hass.sidebar.defaultPanel panel.url_path === hass.defaultPanel
? hass.localize("panel.states") ? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) || : hass.localize(`panel.${panel.title}`) ||
panel.title || panel.title ||

View File

@@ -50,7 +50,6 @@ import type { HaMdListItem } from "./ha-md-list-item";
import "./ha-spinner"; import "./ha-spinner";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
import { DEFAULT_PANEL } from "../data/panel";
const SHOW_AFTER_SPACER = ["config", "developer-tools"]; const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@@ -141,9 +140,9 @@ const defaultPanelSorter = (
export const computePanels = memoizeOne( export const computePanels = memoizeOne(
( (
panels: HomeAssistant["panels"], panels: HomeAssistant["panels"],
defaultPanel: HomeAssistant["sidebar"]["defaultPanel"], defaultPanel: HomeAssistant["defaultPanel"],
panelsOrder: HomeAssistant["sidebar"]["panelOrder"], panelsOrder: string[],
hiddenPanels: HomeAssistant["sidebar"]["hiddenPanels"], hiddenPanels: string[],
locale: HomeAssistant["locale"] locale: HomeAssistant["locale"]
): [PanelInfo[], PanelInfo[]] => { ): [PanelInfo[], PanelInfo[]] => {
if (!panels) { if (!panels) {
@@ -196,6 +195,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@state() private _issuesCount = 0; @state() private _issuesCount = 0;
@state() private _panelOrder?: string[];
@state() private _hiddenPanels?: string[];
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
private _tooltipHideTimeout?: number; private _tooltipHideTimeout?: number;
@@ -210,32 +213,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.connection, this.hass.connection,
"sidebar", "sidebar",
({ value }) => { ({ value }) => {
let panelOrder = value?.panelOrder; this._panelOrder = value?.panelOrder;
let hiddenPanels = value?.hiddenPanels; this._hiddenPanels = value?.hiddenPanels;
let defaultPanel = value?.defaultPanel;
// fallback to old localStorage values // fallback to old localStorage values
if (!panelOrder) { if (!this._panelOrder) {
const storedOrder = localStorage.getItem("sidebarPanelOrder"); const storedOrder = localStorage.getItem("sidebarPanelOrder");
panelOrder = storedOrder ? JSON.parse(storedOrder) : []; this._panelOrder = storedOrder ? JSON.parse(storedOrder) : [];
} }
if (!hiddenPanels) { if (!this._hiddenPanels) {
const storedHidden = localStorage.getItem("sidebarHiddenPanels"); const storedHidden = localStorage.getItem("sidebarHiddenPanels");
hiddenPanels = storedHidden ? JSON.parse(storedHidden) : []; this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : [];
} }
if (!defaultPanel) {
const storedDefault = localStorage.getItem("defaultPanel");
defaultPanel = storedDefault
? JSON.parse(storedDefault)
: DEFAULT_PANEL;
}
fireEvent(this, "hass-set-sidebar-data", {
...value,
defaultPanel: defaultPanel as string,
panelOrder: panelOrder as string[],
hiddenPanels: hiddenPanels as string[],
});
} }
), ),
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
@@ -286,8 +275,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("_updatesCount") || changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") || changedProps.has("_issuesCount") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
(changedProps.has("hass") && changedProps.has("_hiddenPanels") ||
changedProps.get("hass")?.sidebar !== this.hass.sidebar) changedProps.has("_panelOrder")
) { ) {
return true; return true;
} }
@@ -306,7 +295,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.locale !== oldHass.locale || hass.locale !== oldHass.locale ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.sidebar !== oldHass.sidebar || hass.defaultPanel !== oldHass.defaultPanel ||
hass.connected !== oldHass.connected hass.connected !== oldHass.connected
); );
} }
@@ -376,7 +365,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
} }
private _renderAllPanels(selectedPanel: string) { private _renderAllPanels(selectedPanel: string) {
if (!this.hass.sidebar.panelOrder || !this.hass.sidebar.hiddenPanels) { if (!this._panelOrder || !this._hiddenPanels) {
return html` return html`
<ha-fade-in .delay=${500} <ha-fade-in .delay=${500}
><ha-spinner size="small"></ha-spinner ><ha-spinner size="small"></ha-spinner
@@ -386,9 +375,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
const [beforeSpacer, afterSpacer] = computePanels( const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels, this.hass.panels,
this.hass.sidebar.defaultPanel, this.hass.defaultPanel,
this.hass.sidebar.panelOrder, this._panelOrder,
this.hass.sidebar.hiddenPanels, this._hiddenPanels,
this.hass.locale this.hass.locale
); );
@@ -413,11 +402,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return panels.map((panel) => return panels.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path, panel.url_path,
panel.url_path === this.hass.sidebar.defaultPanel panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states") ? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title, : this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon, panel.icon,
panel.url_path === this.hass.sidebar.defaultPanel && !panel.icon panel.url_path === this.hass.defaultPanel && !panel.icon
? PANEL_ICONS.lovelace ? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS : panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path] ? PANEL_ICONS[panel.url_path]

View File

@@ -308,6 +308,10 @@ class HaWebRtcPlayer extends LitElement {
if (!this._remoteStream) { if (!this._remoteStream) {
return; return;
} }
// If the track is audio and the player is muted, we do not add it to the stream.
if (event.track.kind === "audio" && this.muted) {
return;
}
this._remoteStream.addTrack(event.track); this._remoteStream.addTrack(event.track);
if (!this.hasUpdated) { if (!this.hasUpdated) {
await this.updateComplete; await this.updateComplete;

View File

@@ -134,7 +134,8 @@ export interface ConversationChatLogToolResultDelta {
interface PipelineIntentProgressEvent extends PipelineEventBase { interface PipelineIntentProgressEvent extends PipelineEventBase {
type: "intent-progress"; type: "intent-progress";
data: { data: {
chat_log_delta: tts_start_streaming?: boolean;
chat_log_delta?:
| Partial<ConversationChatLogAssistantDelta> | Partial<ConversationChatLogAssistantDelta>
// These always come in 1 chunk // These always come in 1 chunk
| ConversationChatLogToolResultDelta; | ConversationChatLogToolResultDelta;

View File

@@ -26,6 +26,7 @@ import {
import type { EntityRegistryEntry } from "./entity_registry"; import type { EntityRegistryEntry } from "./entity_registry";
import type { FrontendLocaleData } from "./translation"; import type { FrontendLocaleData } from "./translation";
import { isTriggerList } from "./trigger"; import { isTriggerList } from "./trigger";
import { hasTemplate } from "../common/string/has-template";
const triggerTranslationBaseKey = const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type"; "ui.panel.config.automation.editor.triggers.type";
@@ -820,6 +821,12 @@ const tryDescribeCondition = (
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false ignoreAlias = false
) => { ) => {
if (typeof condition === "string" && hasTemplate(condition)) {
return hass.localize(
`${conditionsTranslationBaseKey}.template.description.full`
);
}
if (condition.alias && !ignoreAlias) { if (condition.alias && !ignoreAlias) {
return condition.alias; return condition.alias;
} }

View File

@@ -339,7 +339,7 @@ export const computeBackupSize = (backup: BackupContent) =>
export type BackupType = "automatic" | "manual" | "addon_update"; export type BackupType = "automatic" | "manual" | "addon_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"]; const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "addon_update", "manual"];
export const getBackupTypes = memoize((isHassio: boolean) => export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio isHassio

View File

@@ -679,7 +679,9 @@ export const getEnergyDataCollection = (
const period = const period =
preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod; preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod;
[collection.start, collection.end] = calcDateRange(hass, period); const [start, end] = calcDateRange(hass, period);
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
collection.end = calcDate(end, endOfDay, hass.locale, hass.config);
const scheduleUpdatePeriod = () => { const scheduleUpdatePeriod = () => {
collection._updatePeriodTimeout = window.setTimeout( collection._updatePeriodTimeout = window.setTimeout(

View File

@@ -8,7 +8,6 @@ export interface CoreFrontendUserData {
export interface SidebarFrontendUserData { export interface SidebarFrontendUserData {
panelOrder: string[]; panelOrder: string[];
hiddenPanels: string[]; hiddenPanels: string[];
defaultPanel?: string;
} }
declare global { declare global {

View File

@@ -1,3 +1,4 @@
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant, PanelInfo } from "../types"; import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */ /** Panel to show when no panel is picked. */
@@ -9,9 +10,16 @@ export const getStorageDefaultPanelUrlPath = (): string => {
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL; return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
}; };
export const setDefaultPanel = (
element: HTMLElement,
urlPath: string
): void => {
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
};
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
hass.panels[hass.sidebar.defaultPanel] hass.panels[hass.defaultPanel]
? hass.panels[hass.sidebar.defaultPanel] ? hass.panels[hass.defaultPanel]
: hass.panels[DEFAULT_PANEL]; : hass.panels[DEFAULT_PANEL];
export const getPanelNameTranslationKey = (panel: PanelInfo) => { export const getPanelNameTranslationKey = (panel: PanelInfo) => {

View File

@@ -29,6 +29,7 @@ import { migrateAutomationTrigger } from "./automation";
import type { BlueprintInput } from "./blueprint"; import type { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id"; import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params"; import { createSearchParam } from "../common/url/search-params";
import { hasTemplate } from "../common/string/has-template";
export const MODES = ["single", "restart", "queued", "parallel"] as const; export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const; export const MODES_MAX = ["queued", "parallel"] as const;
@@ -339,6 +340,9 @@ export const getScriptEditorInitData = () => {
export const getActionType = (action: Action): ActionType => { export const getActionType = (action: Action): ActionType => {
// Check based on config_validation.py#determine_script_action // Check based on config_validation.py#determine_script_action
if (typeof action === "string" && hasTemplate(action)) {
return "check_condition";
}
if ("delay" in action) { if ("delay" in action) {
return "delay"; return "delay";
} }

View File

@@ -96,7 +96,7 @@ class DialogEditSidebar extends LitElement {
const [beforeSpacer, afterSpacer] = computePanels( const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels, this.hass.panels,
this.hass.sidebar.defaultPanel, this.hass.defaultPanel,
this._order, this._order,
this._hidden, this._hidden,
this.hass.locale this.hass.locale
@@ -109,12 +109,12 @@ class DialogEditSidebar extends LitElement {
].map((panel) => ({ ].map((panel) => ({
value: panel.url_path, value: panel.url_path,
label: label:
panel.url_path === this.hass.sidebar.defaultPanel panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states") ? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?", : this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
icon: panel.icon || undefined, icon: panel.icon || undefined,
iconPath: iconPath:
panel.url_path === this.hass.sidebar.defaultPanel && !panel.icon panel.url_path === this.hass.defaultPanel && !panel.icon
? PANEL_ICONS.lovelace ? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS : panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path] ? PANEL_ICONS[panel.url_path]
@@ -195,7 +195,6 @@ class DialogEditSidebar extends LitElement {
await saveFrontendUserData(this.hass.connection, "sidebar", { await saveFrontendUserData(this.hass.connection, "sidebar", {
panelOrder: this._order!, panelOrder: this._order!,
hiddenPanels: this._hidden!, hiddenPanels: this._hidden!,
defaultPanel: this.hass.sidebar.defaultPanel,
}); });
} catch (err: any) { } catch (err: any) {
this._error = err.message || err; this._error = err.message || err;

View File

@@ -127,16 +127,15 @@ export class HaConfigDevicePage extends LitElement {
@state() private _related?: RelatedResult; @state() private _related?: RelatedResult;
// If a number, it's the request ID so we make sure we don't show older info @state() private _diagnosticDownloadLinks: DeviceAction[] = [];
@state() private _diagnosticDownloadLinks?: number | DeviceAction[];
@state() private _deleteButtons?: DeviceAction[]; @state() private _deleteButtons: DeviceAction[] = [];
@state() private _deviceActions?: DeviceAction[]; @state() private _deviceActions: DeviceAction[] = [];
@state() private _deviceAlerts?: DeviceAlert[]; @state() private _deviceAlerts: DeviceAlert[] = [];
private _deviceAlertsTimeout?: number; private _deviceAlertsActionsTimeout?: number;
@state() @state()
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
@@ -255,42 +254,19 @@ export class HaConfigDevicePage extends LitElement {
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if ( if (changedProps.has("deviceId") || changedProps.has("entries")) {
changedProps.has("deviceId") || this._deviceActions = [];
changedProps.has("devices") || this._deviceAlerts = [];
changedProps.has("entries") this._deleteButtons = [];
) { this._diagnosticDownloadLinks = [];
this._diagnosticDownloadLinks = undefined; this._fetchData();
this._deleteButtons = undefined;
this._deviceActions = undefined;
this._deviceAlerts = undefined;
} }
if (
(this._diagnosticDownloadLinks &&
this._deleteButtons &&
this._deviceActions &&
this._deviceAlerts) ||
!this.deviceId ||
!this.entries
) {
return;
}
this._diagnosticDownloadLinks = Math.random();
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
this._deviceActions = [];
this._deviceAlerts = [];
this._getDiagnosticButtons(this._diagnosticDownloadLinks);
this._getDeleteActions();
this._getDeviceActions();
clearTimeout(this._deviceAlertsTimeout);
this._getDeviceAlerts();
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog(); loadDeviceRegistryDetailDialog();
this._fetchData();
} }
protected updated(changedProps) { protected updated(changedProps) {
@@ -302,7 +278,7 @@ export class HaConfigDevicePage extends LitElement {
public disconnectedCallback() { public disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
clearTimeout(this._deviceAlertsTimeout); clearTimeout(this._deviceAlertsActionsTimeout);
} }
protected render() { protected render() {
@@ -909,7 +885,18 @@ export class HaConfigDevicePage extends LitElement {
</hass-subpage>`; </hass-subpage>`;
} }
private async _getDiagnosticButtons(requestId: number): Promise<void> { private _fetchData() {
if (this.deviceId && this.entries.length) {
this._getDiagnosticButtons();
this._getDeleteActions();
clearTimeout(this._deviceAlertsActionsTimeout);
this._getDeviceActions();
this._getDeviceAlerts();
}
}
private async _getDiagnosticButtons(): Promise<void> {
const deviceId = this.deviceId;
if (!isComponentLoaded(this.hass, "diagnostics")) { if (!isComponentLoaded(this.hass, "diagnostics")) {
return; return;
} }
@@ -951,7 +938,8 @@ export class HaConfigDevicePage extends LitElement {
links = links.filter(Boolean); links = links.filter(Boolean);
if (this._diagnosticDownloadLinks !== requestId) { if (this.deviceId !== deviceId) {
// abort if the device has changed
return; return;
} }
if (links.length > 0) { if (links.length > 0) {
@@ -1176,12 +1164,12 @@ export class HaConfigDevicePage extends LitElement {
deviceAlerts.push(...alerts); deviceAlerts.push(...alerts);
} }
this._deviceAlerts = deviceAlerts;
if (deviceAlerts.length) { if (deviceAlerts.length) {
this._deviceAlerts = deviceAlerts; this._deviceAlertsActionsTimeout = window.setTimeout(() => {
this._deviceAlertsTimeout = window.setTimeout( this._getDeviceAlerts();
() => this._getDeviceAlerts(), this._getDeviceActions();
DEVICE_ALERTS_INTERVAL }, DEVICE_ALERTS_INTERVAL);
);
} }
} }

View File

@@ -429,7 +429,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
</ha-card> </ha-card>
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
<h1>Third-Party Data Reporting</h1> <h1>Third-party data reporting</h1>
${this._dataCollectionOptIn !== undefined ${this._dataCollectionOptIn !== undefined
? html` ? html`
<ha-switch <ha-switch

View File

@@ -13,11 +13,10 @@ import type {
LovelaceDashboardCreateParams, LovelaceDashboardCreateParams,
LovelaceDashboardMutableParams, LovelaceDashboardMutableParams,
} from "../../../../data/lovelace/dashboard"; } from "../../../../data/lovelace/dashboard";
import { DEFAULT_PANEL } from "../../../../data/panel"; import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail"; import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
import { saveFrontendUserData } from "../../../../data/frontend";
@customElement("dialog-lovelace-dashboard-detail") @customElement("dialog-lovelace-dashboard-detail")
export class DialogLovelaceDashboardDetail extends LitElement { export class DialogLovelaceDashboardDetail extends LitElement {
@@ -60,7 +59,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
if (!this._params || !this._data) { if (!this._params || !this._data) {
return nothing; return nothing;
} }
const defaultPanelUrlPath = this.hass.sidebar.defaultPanel; const defaultPanelUrlPath = this.hass.defaultPanel;
const titleInvalid = !this._data.title || !this._data.title.trim(); const titleInvalid = !this._data.title || !this._data.title.trim();
return html` return html`
@@ -250,17 +249,15 @@ export class DialogLovelaceDashboardDetail extends LitElement {
}; };
} }
private async _toggleDefault() { private _toggleDefault() {
const urlPath = this._params?.urlPath; const urlPath = this._params?.urlPath;
if (!urlPath) { if (!urlPath) {
return; return;
} }
await saveFrontendUserData(this.hass!.connection, "sidebar", { setDefaultPanel(
panelOrder: this.hass!.sidebar.panelOrder, this,
hiddenPanels: this.hass!.sidebar.hiddenPanels, urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
defaultPanel: );
urlPath === this.hass.sidebar.defaultPanel ? DEFAULT_PANEL : urlPath,
});
} }
private async _updateDashboard() { private async _updateDashboard() {

View File

@@ -255,7 +255,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
const defaultMode = ( const defaultMode = (
this.hass.panels?.lovelace?.config as LovelacePanelConfig this.hass.panels?.lovelace?.config as LovelacePanelConfig
).mode; ).mode;
const defaultUrlPath = this.hass.sidebar.defaultPanel; const defaultUrlPath = this.hass.defaultPanel;
const isDefault = defaultUrlPath === "lovelace"; const isDefault = defaultUrlPath === "lovelace";
const result: DataTableItem[] = [ const result: DataTableItem[] = [
{ {

View File

@@ -64,7 +64,9 @@ export class EnergyViewStrategy extends ReactiveElement {
(source) => source.type === "solar" (source) => source.type === "solar"
); );
const hasGas = prefs.energy_sources.some((source) => source.type === "gas"); const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
const hasWater = prefs.energy_sources.some( const hasWater = prefs.energy_sources.some(
(source) => source.type === "water" (source) => source.type === "water"
); );
@@ -74,8 +76,8 @@ export class EnergyViewStrategy extends ReactiveElement {
collection_key: "energy_dashboard", collection_key: "energy_dashboard",
}); });
// Only include if we have a grid source. // Only include if we have a grid or battery.
if (hasGrid) { if (hasGrid || hasBattery) {
view.cards!.push({ view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"), title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
type: "energy-usage-graph", type: "energy-usage-graph",
@@ -110,8 +112,8 @@ export class EnergyViewStrategy extends ReactiveElement {
}); });
} }
// Only include if we have a grid. // Only include if we have a grid or battery.
if (hasGrid) { if (hasGrid || hasBattery) {
view.cards!.push({ view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"), title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution", type: "energy-distribution",
@@ -120,7 +122,7 @@ export class EnergyViewStrategy extends ReactiveElement {
}); });
} }
if (hasGrid || hasSolar || hasGas || hasWater) { if (hasGrid || hasSolar || hasGas || hasWater || hasBattery) {
view.cards!.push({ view.cards!.push({
title: hass.localize( title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title" "ui.panel.energy.cards.energy_sources_table_title"

View File

@@ -10,6 +10,8 @@ import {
addYears, addYears,
addMonths, addMonths,
addHours, addHours,
startOfDay,
addDays,
} from "date-fns"; } from "date-fns";
import type { import type {
BarSeriesOption, BarSeriesOption,
@@ -282,6 +284,10 @@ export function getCompareTransform(start: Date, compareStart?: Date) {
) { ) {
return (ts: Date) => addMonths(ts, compareMonthDiff); return (ts: Date) => addMonths(ts, compareMonthDiff);
} }
const compareDayDiff = differenceInDays(start, compareStart);
if (compareDayDiff !== 0 && start.getTime() === startOfDay(start).getTime()) {
return (ts: Date) => addDays(ts, compareDayDiff);
}
const compareOffset = start.getTime() - compareStart.getTime(); const compareOffset = start.getTime() - compareStart.getTime();
return (ts: Date) => addMilliseconds(ts, compareOffset); return (ts: Date) => addMilliseconds(ts, compareOffset);
} }

View File

@@ -102,14 +102,13 @@ class HuiEnergyDistrubutionCard
const prefs = this._data.prefs; const prefs = this._data.prefs;
const types = energySourcesByType(prefs); const types = energySourcesByType(prefs);
// The strategy only includes this card if we have a grid. const hasGrid =
const hasConsumption = true; !!types.grid?.[0].flow_from.length || !!types.grid?.[0].flow_to.length;
const hasSolarProduction = types.solar !== undefined; const hasSolarProduction = types.solar !== undefined;
const hasBattery = types.battery !== undefined; const hasBattery = types.battery !== undefined;
const hasGas = types.gas !== undefined; const hasGas = types.gas !== undefined;
const hasWater = types.water !== undefined; const hasWater = types.water !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; const hasReturnToGrid = !!types.grid?.[0].flow_to.length;
const { summedData, compareSummedData: _ } = getSummedData(this._data); const { summedData, compareSummedData: _ } = getSummedData(this._data);
const { consumption, compareConsumption: __ } = computeConsumptionData( const { consumption, compareConsumption: __ } = computeConsumptionData(
@@ -163,14 +162,14 @@ class HuiEnergyDistrubutionCard
} }
let batteryFromGrid: null | number = null; let batteryFromGrid: null | number = null;
let batteryToGrid: null | number = null; let batteryToGrid: null | number = null;
if (hasBattery) { if (hasBattery && hasGrid) {
batteryToGrid = consumption.total.battery_to_grid; batteryToGrid = consumption.total.battery_to_grid;
batteryFromGrid = consumption.total.grid_to_battery; batteryFromGrid = consumption.total.grid_to_battery;
} }
let solarToBattery: null | number = null; let solarToBattery: null | number = null;
let solarToGrid: null | number = null; let solarToGrid: null | number = null;
if (hasSolarProduction) { if (hasSolarProduction && hasGrid) {
solarToGrid = consumption.total.solar_to_grid; solarToGrid = consumption.total.solar_to_grid;
} }
if (hasSolarProduction && hasBattery) { if (hasSolarProduction && hasBattery) {
@@ -182,7 +181,9 @@ class HuiEnergyDistrubutionCard
batteryConsumption = Math.max(consumption.total.used_battery, 0); batteryConsumption = Math.max(consumption.total.used_battery, 0);
} }
const gridConsumption = Math.max(consumption.total.used_grid, 0); const gridConsumption = hasGrid
? Math.max(consumption.total.used_grid, 0)
: 0;
const totalHomeConsumption = Math.max(0, consumption.total.used_total); const totalHomeConsumption = Math.max(0, consumption.total.used_total);
@@ -206,7 +207,11 @@ class HuiEnergyDistrubutionCard
// This fallback is used in the demo // This fallback is used in the demo
let electricityMapUrl = "https://app.electricitymap.org"; let electricityMapUrl = "https://app.electricitymap.org";
if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) { if (
hasGrid &&
this._data.co2SignalEntity &&
this._data.fossilEnergyConsumption
) {
// Calculate high carbon consumption // Calculate high carbon consumption
const highCarbonEnergy = Object.values( const highCarbonEnergy = Object.values(
this._data.fossilEnergyConsumption this._data.fossilEnergyConsumption
@@ -225,7 +230,7 @@ class HuiEnergyDistrubutionCard
if (gridConsumption !== totalFromGrid) { if (gridConsumption !== totalFromGrid) {
// Only get the part that was used for consumption and not the battery // Only get the part that was used for consumption and not the battery
highCarbonConsumption = highCarbonConsumption =
highCarbonEnergy * (gridConsumption / totalFromGrid); highCarbonEnergy * (gridConsumption! / totalFromGrid);
} else { } else {
highCarbonConsumption = highCarbonEnergy; highCarbonConsumption = highCarbonEnergy;
} }
@@ -378,41 +383,43 @@ class HuiEnergyDistrubutionCard
</div>` </div>`
: ""} : ""}
<div class="row"> <div class="row">
<div class="circle-container grid"> ${hasGrid
<div class="circle"> ? html`<div class="circle-container grid">
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon> <div class="circle">
${returnedToGrid !== null <ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
? html`<span class="return"> ${returnedToGrid !== null
<ha-svg-icon ? html`<span class="return">
class="small" <ha-svg-icon
.path=${mdiArrowLeft} class="small"
></ha-svg-icon .path=${mdiArrowLeft}
>${formatConsumptionShort( ></ha-svg-icon
>${formatConsumptionShort(
this.hass,
returnedToGrid,
"kWh"
)}
</span>`
: ""}
<span class="consumption">
${hasReturnToGrid
? html`<ha-svg-icon
class="small"
.path=${mdiArrowRight}
></ha-svg-icon>`
: ""}${formatConsumptionShort(
this.hass, this.hass,
returnedToGrid, totalFromGrid,
"kWh" "kWh"
)} )}
</span>` </span>
: ""} </div>
<span class="consumption"> <span class="label"
${hasReturnToGrid >${this.hass.localize(
? html`<ha-svg-icon "ui.panel.lovelace.cards.energy.energy_distribution.grid"
class="small" )}</span
.path=${mdiArrowRight} >
></ha-svg-icon>` </div> `
: ""}${formatConsumptionShort( : html`<div class="grid-spacer"></div>`}
this.hass,
totalFromGrid,
"kWh"
)}
</span>
</div>
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
)}</span
>
</div>
<div class="circle-container home"> <div class="circle-container home">
<div <div
class="circle ${classMap({ class="circle ${classMap({
@@ -480,22 +487,27 @@ class HuiEnergyDistrubutionCard
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
/>` />`
: ""} : ""}
<circle ${hasGrid
? svg`<circle
class="grid" class="grid"
cx="40" cx="40"
cy="40" cy="40"
r="38" r="38"
stroke-dasharray="${homeHighCarbonCircumference ?? stroke-dasharray="${
CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference ??
homeSolarCircumference! - CIRCLE_CIRCUMFERENCE -
(homeBatteryCircumference || homeSolarCircumference! -
0)} ${homeHighCarbonCircumference !== undefined (homeBatteryCircumference || 0)
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference } ${
: homeSolarCircumference! + homeHighCarbonCircumference !== undefined
(homeBatteryCircumference || 0)}" ? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
: homeSolarCircumference! +
(homeBatteryCircumference || 0)
}"
stroke-dashoffset="0" stroke-dashoffset="0"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
/> />`
: nothing}
</svg>` </svg>`
: ""} : ""}
</div> </div>
@@ -619,15 +631,19 @@ class HuiEnergyDistrubutionCard
d="M55,100 v-15 c0,-35 10,-30 30,-30 h20" d="M55,100 v-15 c0,-35 10,-30 30,-30 h20"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
></path> ></path>
<path ${
id="battery-grid" hasGrid
class=${classMap({ ? svg`<path
"battery-from-grid": Boolean(batteryFromGrid), id="battery-grid"
"battery-to-grid": Boolean(batteryToGrid), class=${classMap({
})} "battery-from-grid": Boolean(batteryFromGrid),
d="M45,100 v-15 c0,-35 -10,-30 -30,-30 h-20" "battery-to-grid": Boolean(batteryToGrid),
vector-effect="non-scaling-stroke" })}
></path> d="M45,100 v-15 c0,-35 -10,-30 -30,-30 h-20"
vector-effect="non-scaling-stroke"
></path>`
: nothing
}
` `
: ""} : ""}
${hasBattery && hasSolarProduction ${hasBattery && hasSolarProduction
@@ -638,12 +654,14 @@ class HuiEnergyDistrubutionCard
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
></path>` ></path>`
: ""} : ""}
<path ${hasGrid
class="grid" ? svg`<path
id="grid" class="grid"
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100" id="grid"
vector-effect="non-scaling-stroke" d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
></path> vector-effect="non-scaling-stroke"
></path>`
: nothing}
${solarToGrid && this._animate ${solarToGrid && this._animate
? svg`<circle ? svg`<circle
r="1" r="1"
@@ -839,6 +857,10 @@ class HuiEnergyDistrubutionCard
.spacer { .spacer {
width: 84px; width: 84px;
} }
.grid-spacer {
width: 84px;
height: 100px;
}
.circle { .circle {
width: 80px; width: 80px;
height: 80px; height: 80px;

View File

@@ -418,12 +418,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
.keypad { .keypad {
--keypad-columns: 3; --keypad-columns: 3;
margin-top: 12px;
padding: 12px; padding: 12px;
display: grid; display: grid;
grid-template-columns: repeat(var(--keypad-columns), auto); grid-template-columns: repeat(var(--keypad-columns), auto);
grid-auto-rows: auto; grid-auto-rows: auto;
grid-gap: 24px; grid-gap: 16px;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
} }

View File

@@ -12,7 +12,8 @@ import {
attachConditionMediaQueriesListeners, attachConditionMediaQueriesListeners,
checkConditionsMet, checkConditionsMet,
} from "../common/validate-condition"; } from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element"; import { tryCreateCardElement } from "../create-element/create-card-element";
import { createErrorCardElement } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceGridOptions } from "../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types";
declare global { declare global {
@@ -71,10 +72,23 @@ export class HuiCard extends ReactiveElement {
public getGridOptions(): LovelaceGridOptions { public getGridOptions(): LovelaceGridOptions {
const elementOptions = this.getElementGridOptions(); const elementOptions = this.getElementGridOptions();
const configOptions = this.getConfigGridOptions(); const configOptions = this.getConfigGridOptions();
return { const mergedConfig = {
...elementOptions, ...elementOptions,
...configOptions, ...configOptions,
}; };
// If the element has fixed rows or columns, we use the values from the element
if (elementOptions.fixed_rows) {
mergedConfig.rows = elementOptions.rows;
delete mergedConfig.min_rows;
delete mergedConfig.max_rows;
}
if (elementOptions.fixed_columns) {
mergedConfig.columns = elementOptions.columns;
delete mergedConfig.min_columns;
delete mergedConfig.max_columns;
}
return mergedConfig;
} }
// options provided by the element // options provided by the element
@@ -82,7 +96,9 @@ export class HuiCard extends ReactiveElement {
if (!this._element) return {}; if (!this._element) return {};
if (this._element.getGridOptions) { if (this._element.getGridOptions) {
return this._element.getGridOptions(); const options = this._element.getGridOptions();
// Some custom cards might return undefined, so we ensure we return an object
return options || {};
} }
if (this._element.getLayoutOptions) { if (this._element.getLayoutOptions) {
// Disabled for now to avoid spamming the console, need to be re-enabled when hui-card performance are fixed // Disabled for now to avoid spamming the console, need to be re-enabled when hui-card performance are fixed
@@ -119,7 +135,15 @@ export class HuiCard extends ReactiveElement {
} }
private _loadElement(config: LovelaceCardConfig) { private _loadElement(config: LovelaceCardConfig) {
this._element = createCardElement(config); try {
this._element = tryCreateCardElement(config);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : undefined;
this._element = createErrorCardElement({
type: "error",
message: errorMessage,
});
}
this._elementConfig = config; this._elementConfig = config;
if (this.hass) { if (this.hass) {
this._element.hass = this.hass; this._element.hass = this.hass;
@@ -200,6 +224,7 @@ export class HuiCard extends ReactiveElement {
this._element.preview = this.preview; this._element.preview = this.preview;
// For backwards compatibility // For backwards compatibility
(this._element as any).editMode = this.preview; (this._element as any).editMode = this.preview;
fireEvent(this, "card-updated");
} catch (e: any) { } catch (e: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(this.config?.type, e); console.error(this.config?.type, e);

View File

@@ -1,11 +1,11 @@
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js"; import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCard, LovelaceGridOptions } from "../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { ErrorCardConfig } from "./types"; import type { ErrorCardConfig } from "./types";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
const ERROR_ICONS = { const ERROR_ICONS = {
warning: mdiAlertOutline, warning: mdiAlertOutline,
@@ -30,9 +30,10 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
public getGridOptions(): LovelaceGridOptions { public getGridOptions(): LovelaceGridOptions {
return { return {
columns: 6, columns: 6,
rows: 1, rows: this.preview ? "auto" : 1,
min_rows: 1, min_rows: 1,
min_columns: 6, min_columns: 6,
fixed_rows: this.preview,
}; };
} }
@@ -45,17 +46,24 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
const error = const error =
this._config?.error || this._config?.error ||
this.hass?.localize("ui.errors.config.configuration_error"); this.hass?.localize("ui.errors.config.configuration_error");
const showTitle = this.hass === undefined || this.hass?.user?.is_admin; const showTitle =
this.hass === undefined || this.hass?.user?.is_admin || this.preview;
const showMessage = this.preview;
return html` return html`
<ha-card class="${this.severity} ${showTitle ? "" : "no-title"}"> <ha-card class="${this.severity} ${showTitle ? "" : "no-title"}">
<div class="icon"> <div class="header">
<slot name="icon"> <div class="icon">
<ha-svg-icon .path=${ERROR_ICONS[this.severity]}></ha-svg-icon> <slot name="icon">
</slot> <ha-svg-icon .path=${ERROR_ICONS[this.severity]}></ha-svg-icon>
</slot>
</div>
${showTitle
? html`<div class="title"><slot>${error}</slot></div>`
: nothing}
</div> </div>
${showTitle ${showMessage && this._config?.message
? html`<div class="title"><slot>${error}</slot></div>` ? html`<div class="message">${this._config.message}</div>`
: nothing} : nothing}
</ha-card> </ha-card>
`; `;
@@ -65,10 +73,6 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
ha-card { ha-card {
height: 100%; height: 100%;
border-width: 0; border-width: 0;
display: flex;
align-items: center;
column-gap: 16px;
padding: 16px;
} }
ha-card::after { ha-card::after {
position: absolute; position: absolute;
@@ -81,6 +85,15 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
content: ""; content: "";
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
} }
.header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
.message {
padding: 0 16px 16px 16px;
}
.no-title { .no-title {
justify-content: center; justify-content: center;
} }
@@ -90,13 +103,13 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: var(--ha-font-weight-bold); font-weight: var(--ha-font-weight-bold);
} }
ha-card.warning > .icon { ha-card.warning .icon {
color: var(--warning-color); color: var(--warning-color);
} }
ha-card.warning::after { ha-card.warning::after {
background-color: var(--warning-color); background-color: var(--warning-color);
} }
ha-card.error > .icon { ha-card.error .icon {
color: var(--error-color); color: var(--error-color);
} }
ha-card.error::after { ha-card.error::after {

View File

@@ -85,7 +85,7 @@ export class HuiBadgeEditMode extends LitElement {
if (this._touchStarted) return; if (this._touchStarted) return;
this._hover = true; this._hover = true;
}); });
this.addEventListener("mouseout", () => { this.addEventListener("mouseleave", () => {
this._hover = false; this._hover = false;
}); });
this.addEventListener("click", () => { this.addEventListener("click", () => {

View File

@@ -71,7 +71,7 @@ export class HuiCardEditMode extends LitElement {
if (this._touchStarted) return; if (this._touchStarted) return;
this._hover = true; this._hover = true;
}); });
this.addEventListener("mouseout", () => { this.addEventListener("mouseleave", () => {
this._hover = false; this._hover = false;
}); });
this.addEventListener("click", () => { this.addEventListener("click", () => {

View File

@@ -297,24 +297,17 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
} }
private _dateRangeChanged(ev) { private _dateRangeChanged(ev) {
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
this._startDate = calcDate( this._startDate = calcDate(
ev.detail.value.startDate, ev.detail.value.startDate,
startOfDay, startOfDay,
this.hass.locale, this.hass.locale,
this.hass.config, this.hass.config
{
weekStartsOn,
}
); );
this._endDate = calcDate( this._endDate = calcDate(
ev.detail.value.endDate, ev.detail.value.endDate,
endOfDay, endOfDay,
this.hass.locale, this.hass.locale,
this.hass.config, this.hass.config
{
weekStartsOn,
}
); );
this._updateCollectionPeriod(); this._updateCollectionPeriod();

View File

@@ -139,7 +139,7 @@ export class HuiDialogSelectDashboard extends LitElement {
...(this._params!.dashboards || (await fetchDashboards(this.hass))), ...(this._params!.dashboards || (await fetchDashboards(this.hass))),
]; ];
const currentPath = this._fromUrlPath || this.hass.sidebar.defaultPanel; const currentPath = this._fromUrlPath || this.hass.defaultPanel;
for (const dashboard of this._dashboards!) { for (const dashboard of this._dashboards!) {
if (dashboard.url_path !== currentPath) { if (dashboard.url_path !== currentPath) {
this._toUrlPath = dashboard.url_path; this._toUrlPath = dashboard.url_path;

View File

@@ -77,7 +77,7 @@ export class HuiDialogSelectView extends LitElement {
"ui.panel.lovelace.editor.select_view.dashboard_label" "ui.panel.lovelace.editor.select_view.dashboard_label"
)} )}
.disabled=${!this._dashboards.length} .disabled=${!this._dashboards.length}
.value=${this._urlPath || this.hass.sidebar.defaultPanel} .value=${this._urlPath || this.hass.defaultPanel}
@selected=${this._dashboardChanged} @selected=${this._dashboardChanged}
@closed=${stopPropagation} @closed=${stopPropagation}
fixedMenuPosition fixedMenuPosition

View File

@@ -57,6 +57,7 @@ import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box"; } from "../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
import { import {
QuickBarMode, QuickBarMode,
showQuickBar, showQuickBar,
@@ -75,9 +76,9 @@ import { getLovelaceStrategy } from "./strategies/get-strategy";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy"; import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import type { Lovelace } from "./types"; import type { Lovelace } from "./types";
import "./views/hui-view"; import "./views/hui-view";
import "./views/hui-view-container";
import type { HUIView } from "./views/hui-view"; import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background"; import "./views/hui-view-background";
import "./views/hui-view-container";
@customElement("hui-root") @customElement("hui-root")
class HUIRoot extends LitElement { class HUIRoot extends LitElement {
@@ -490,7 +491,16 @@ class HUIRoot extends LitElement {
} else if (searchParams.conversation === "1") { } else if (searchParams.conversation === "1") {
this._clearParam("conversation"); this._clearParam("conversation");
this._showVoiceCommandDialog(); this._showVoiceCommandDialog();
} else if (searchParams["more-info-entity-id"]) {
const entityId = searchParams["more-info-entity-id"];
this._clearParam("more-info-entity-id");
// Wait for the next render to ensure the view is fully loaded
// because the more info dialog is closed when the url changes
afterNextRender(() => {
this._showMoreInfoDialog(entityId);
});
} }
window.addEventListener("scroll", this._handleWindowScroll, { window.addEventListener("scroll", this._handleWindowScroll, {
passive: true, passive: true,
}); });
@@ -730,6 +740,10 @@ class HUIRoot extends LitElement {
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
} }
private _showMoreInfoDialog(entityId: string): void {
showMoreInfoDialog(this, { entityId });
}
private _handleEnableEditMode(ev: CustomEvent<RequestSelectedDetail>): void { private _handleEnableEditMode(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;

View File

@@ -62,6 +62,8 @@ export interface LovelaceGridOptions {
min_columns?: number; min_columns?: number;
min_rows?: number; min_rows?: number;
max_rows?: number; max_rows?: number;
fixed_rows?: boolean;
fixed_columns?: boolean;
} }
export interface LovelaceCard extends HTMLElement { export interface LovelaceCard extends HTMLElement {

View File

@@ -6,8 +6,8 @@ import "../../components/ha-select";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import type { LovelaceDashboard } from "../../data/lovelace/dashboard"; import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
import { fetchDashboards } from "../../data/lovelace/dashboard"; import { fetchDashboards } from "../../data/lovelace/dashboard";
import { setDefaultPanel } from "../../data/panel";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { saveFrontendUserData } from "../../data/frontend";
@customElement("ha-pick-dashboard-row") @customElement("ha-pick-dashboard-row")
class HaPickDashboardRow extends LitElement { class HaPickDashboardRow extends LitElement {
@@ -37,7 +37,7 @@ class HaPickDashboardRow extends LitElement {
"ui.panel.profile.dashboard.dropdown_label" "ui.panel.profile.dashboard.dropdown_label"
)} )}
.disabled=${!this._dashboards?.length} .disabled=${!this._dashboards?.length}
.value=${this.hass.sidebar.defaultPanel} .value=${this.hass.defaultPanel}
@selected=${this._dashboardChanged} @selected=${this._dashboardChanged}
naturalMenuWidth naturalMenuWidth
> >
@@ -71,16 +71,12 @@ class HaPickDashboardRow extends LitElement {
this._dashboards = await fetchDashboards(this.hass); this._dashboards = await fetchDashboards(this.hass);
} }
private async _dashboardChanged(ev) { private _dashboardChanged(ev) {
const urlPath = ev.target.value; const urlPath = ev.target.value;
if (!urlPath || urlPath === this.hass.sidebar.defaultPanel) { if (!urlPath || urlPath === this.hass.defaultPanel) {
return; return;
} }
await saveFrontendUserData(this.hass!.connection, "sidebar", { setDefaultPanel(this, urlPath);
panelOrder: this.hass!.sidebar.panelOrder,
hiddenPanels: this.hass!.sidebar.hiddenPanels,
defaultPanel: urlPath,
});
} }
} }

View File

@@ -167,10 +167,6 @@ class HaProfileSectionGeneral extends LitElement {
)} )}
</mwc-button> </mwc-button>
</ha-settings-row> </ha-settings-row>
<ha-pick-dashboard-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-dashboard-row>
${this.hass.user!.is_admin ${this.hass.user!.is_admin
? html` ? html`
<ha-advanced-mode-row <ha-advanced-mode-row
@@ -204,6 +200,10 @@ class HaProfileSectionGeneral extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}
></ha-pick-theme-row> ></ha-pick-theme-row>
<ha-pick-dashboard-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-dashboard-row>
${this.hass.dockedSidebar !== "auto" || !this.narrow ${this.hass.dockedSidebar !== "auto" || !this.narrow
? html` ? html`
<ha-force-narrow-row <ha-force-narrow-row

View File

@@ -59,11 +59,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
services: null as any, services: null as any,
user: null as any, user: null as any,
panelUrl: (this as any)._panelUrl, panelUrl: (this as any)._panelUrl,
sidebar: { defaultPanel: DEFAULT_PANEL,
defaultPanel: DEFAULT_PANEL,
hiddenPanels: [],
panelOrder: [],
},
language, language,
selectedLanguage: null, selectedLanguage: null,
locale: { locale: {

View File

@@ -7,16 +7,20 @@ interface DockSidebarParams {
dock: HomeAssistant["dockedSidebar"]; dock: HomeAssistant["dockedSidebar"];
} }
interface DefaultPanelParams {
defaultPanel: HomeAssistant["defaultPanel"];
}
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"hass-dock-sidebar": DockSidebarParams; "hass-dock-sidebar": DockSidebarParams;
"hass-set-sidebar-data": HomeAssistant["sidebar"]; "hass-default-panel": DefaultPanelParams;
} }
// for add event listener // for add event listener
interface HTMLElementEventMap { interface HTMLElementEventMap {
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>; "hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
"hass-set-sidebar-data": HASSDomEvent<HomeAssistant["sidebar"]>; "hass-default-panel": HASSDomEvent<DefaultPanelParams>;
} }
} }
@@ -28,10 +32,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this._updateHass({ dockedSidebar: ev.detail.dock }); this._updateHass({ dockedSidebar: ev.detail.dock });
storeState(this.hass!); storeState(this.hass!);
}); });
this.addEventListener("hass-set-sidebar-data", async (ev) => { this.addEventListener("hass-default-panel", (ev) => {
this._updateHass({ this._updateHass({ defaultPanel: ev.detail.defaultPanel });
sidebar: ev.detail,
});
storeState(this.hass!); storeState(this.hass!);
}); });
} }

View File

@@ -5807,8 +5807,8 @@
"provisioned_devices": "Provisioned devices", "provisioned_devices": "Provisioned devices",
"not_ready": "{count} not ready", "not_ready": "{count} not ready",
"nvm_backup": { "nvm_backup": {
"title": "Backup and Restore", "title": "Backup and restore",
"description": "Back up or restore your Z-Wave controller's Non-Volatile Memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.", "description": "Back up or restore your Z-Wave controller's non-volatile memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
"download_backup": "Download backup", "download_backup": "Download backup",
"restore_backup": "Restore from backup", "restore_backup": "Restore from backup",
"backup_failed": "Failed to download backup", "backup_failed": "Failed to download backup",
@@ -6013,9 +6013,9 @@
"default": "Default" "default": "Default"
}, },
"network_status": { "network_status": {
"connected": "Connected", "connected": "status: connected",
"connecting": "Connecting", "connecting": "status: connecting",
"unknown": "Unknown" "unknown": "status: unknown"
}, },
"add_node": { "add_node": {
"title": "Add a Z-Wave device", "title": "Add a Z-Wave device",

View File

@@ -243,11 +243,7 @@ export interface HomeAssistant {
vibrate: boolean; vibrate: boolean;
debugConnection: boolean; debugConnection: boolean;
dockedSidebar: "docked" | "always_hidden" | "auto"; dockedSidebar: "docked" | "always_hidden" | "auto";
sidebar: { defaultPanel: string;
defaultPanel: string;
panelOrder: string[];
hiddenPanels: string[];
};
moreInfoEntityId: string | null; moreInfoEntityId: string | null;
user?: CurrentUser; user?: CurrentUser;
userData?: CoreFrontendUserData | null; userData?: CoreFrontendUserData | null;

View File

@@ -8,7 +8,7 @@ const STORED_STATE = [
"debugConnection", "debugConnection",
"suspendWhenHidden", "suspendWhenHidden",
"enableShortcuts", "enableShortcuts",
"sidebar", "defaultPanel",
]; ];
export function storeState(hass: HomeAssistant) { export function storeState(hass: HomeAssistant) {

883
yarn.lock

File diff suppressed because it is too large Load Diff