Compare commits

..

10 Commits

Author SHA1 Message Date
Paul Bottein
6745a97f86 Set energy selector position to fixed 2025-12-15 15:52:25 +01:00
Petar Petrov
a8286e31a0 Use dialog backdrop and define default 2025-12-11 12:47:10 +02:00
Petar Petrov
4fa7274fbe fix scrollbar 2025-12-10 12:24:03 +02:00
Petar Petrov
414c1315a4 move to hui-root via slot 2025-12-10 12:08:44 +02:00
Petar Petrov
04bfc350ea Blur view when date picker is open 2025-12-09 15:25:09 +02:00
Petar Petrov
75ec9404ce Sticky period selector for energy dashboard 2025-12-09 13:15:12 +02:00
Aidan Timson
41cabde393 Migrate dialog-download-decrypted-backup to ha-wa-dialog (#28442)
* Migrate dialog-download-decrypted-backup.ts from ha-md-dialog to ha-wa-dialog

* Fixes from other migrations
2025-12-09 09:28:44 +02:00
Aidan Timson
47d1fdf673 Migrate change/show backup encryption key to ha-wa-dialog (#28428)
* Migrate dialog-change-backup-encryption-key.ts from ha-md-dialog to ha-wa-dialog

* Remove open render nothing, remove custom css size

* Migrate show backup key

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-09 07:24:43 +00:00
Aidan Timson
e59e83fffe Migrate dialog-areas-floors-order to ha-wa-dialog (#28424)
* Migrate dialog-areas-floors-order.ts from ha-md-dialog to ha-wa-dialog

* Fix saving

* Remove render nothing if dialog false

* Remove custom width
2025-12-09 09:12:29 +02:00
Aidan Timson
b896b78876 Migrate labs dialogs to ha-wa-dialog (#28429)
* Migrate dialog-labs-preview-feature-enable.ts from ha-md-dialog to ha-wa-dialog

* Migrate dialog-labs-progress.ts from ha-md-dialog to ha-wa-dialog

* Restore

* Remove use of slots

* Fix

* Remove header
2025-12-09 08:55:28 +02:00
19 changed files with 440 additions and 518 deletions

View File

@@ -101,6 +101,10 @@ const Component = Vue.extend({
type: String,
default: "en",
},
opensVertical: {
type: String,
default: undefined,
},
},
render(createElement) {
// @ts-expect-error
@@ -129,6 +133,11 @@ const Component = Vue.extend({
},
expression: "dateRange",
},
on: {
toggle: (open: boolean) => {
fireEvent(this.$el as HTMLElement, "toggle", { open });
},
},
scopedSlots: {
input() {
return createElement("slot", {
@@ -309,6 +318,10 @@ class DateRangePickerElement extends WrappedElement {
min-width: unset !important;
display: block !important;
}
:host([opens-vertical="up"]) .daterangepicker {
bottom: 100%;
top: auto !important;
}
`;
if (mainWindow.document.dir === "rtl") {
style.innerHTML += `
@@ -340,4 +353,7 @@ declare global {
interface HTMLElementTagNameMap {
"date-range-picker": DateRangePickerElement;
}
interface HASSDomEvents {
toggle: { open: boolean };
}
}

View File

@@ -74,6 +74,9 @@ export class HaDateRangePicker extends LitElement {
@property({ attribute: "extended-presets", type: Boolean })
public extendedPresets = false;
@property({ attribute: "vertical-opening-direction" })
public verticalOpeningDirection?: "up" | "down";
@property({ attribute: false }) public openingDirection?:
| "right"
| "left"
@@ -127,6 +130,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}

View File

@@ -1,7 +1,7 @@
import { mdiClose, mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import {
type AreasFloorHierarchy,
@@ -11,12 +11,10 @@ import {
} from "../../../common/areas/areas-floor-hierarchy";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-header";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
@@ -49,8 +47,6 @@ class DialogAreasFloorsOrder extends LitElement {
@state() private _saving = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
_params: AreasFloorsOrderDialogParams
): Promise<void> {
@@ -66,7 +62,8 @@ class DialogAreasFloorsOrder extends LitElement {
}
public closeDialog(): void {
this._dialog?.close();
this._saving = false;
this._open = false;
}
private _dialogClosed(): void {
@@ -77,7 +74,7 @@ class DialogAreasFloorsOrder extends LitElement {
}
protected render() {
if (!this._open || !this._hierarchy) {
if (!this._hierarchy) {
return nothing;
}
@@ -89,17 +86,13 @@ class DialogAreasFloorsOrder extends LitElement {
);
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
</ha-dialog-header>
<div slot="content" class="content">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
@closed=${this._dialogClosed}
>
<div class="content">
<ha-sortable
handle-selector=".floor-handle"
draggable-selector=".floor"
@@ -116,15 +109,23 @@ class DialogAreasFloorsOrder extends LitElement {
</ha-sortable>
${this._renderUnassignedAreas()}
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog} appearance="plain">
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${this._saving}>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -391,15 +392,13 @@ class DialogAreasFloorsOrder extends LitElement {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
min-width: 600px;
ha-wa-dialog {
max-height: 90%;
--dialog-content-padding: 8px 24px;
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
@media all and (max-width: 580px), all and (max-height: 500px) {
ha-wa-dialog {
min-width: 100%;
min-height: 100%;
}

View File

@@ -1,15 +1,14 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -31,20 +30,18 @@ type Step = (typeof STEPS)[number];
class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
@state() private _step?: Step;
@state() private _params?: ChangeBackupEncryptionKeyDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _newEncryptionKey?: string;
public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
this._opened = true;
this._open = true;
this._newEncryptionKey = generateEncryptionKey();
}
@@ -52,10 +49,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
if (this._params!.cancel) {
this._params!.cancel();
}
if (this._opened) {
if (this._open) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._open = false;
this._step = undefined;
this._params = undefined;
this._newEncryptionKey = undefined;
@@ -64,7 +61,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
private _done() {
this._params?.submit!(true);
this._dialog.close();
this.closeDialog();
}
private _previousStep() {
@@ -84,10 +81,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
}
protected render() {
if (!this._opened || !this._params) {
return nothing;
}
const dialogTitle =
this._step === "current" || this._step === "new"
? this.hass.localize(
@@ -96,36 +89,40 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
: "";
return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
${this._step === "new"
? html`
<ha-icon-button-prev
slot="navigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`
: html`
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
`}
<span slot="title">${dialogTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
<div slot="actions">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
prevent-scrim-close
@closed=${this.closeDialog}
>
${this._step === "new"
? html`
<ha-icon-button-prev
slot="headerNavigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`
: html`
<ha-icon-button
slot="headerNavigationIcon"
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
`}
${this._renderStepContent()}
<ha-dialog-footer slot="footer">
${this._step === "current"
? html`
<ha-button @click=${this._nextStep}>
<ha-button slot="primaryAction" @click=${this._nextStep}>
${this.hass.localize("ui.common.next")}
</ha-button>
`
: this._step === "new"
? html`
<ha-button
slot="primaryAction"
@click=${this._submit}
.disabled=${!this._newEncryptionKey}
variant="danger"
@@ -136,14 +133,14 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
</ha-button>
`
: html`
<ha-button @click=${this._done}>
<ha-button slot="primaryAction" @click=${this._done}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.change_encryption_key.actions.done"
)}
</ha-button>
`}
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -287,10 +284,8 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
@@ -321,14 +316,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
flex: none;
margin: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}

View File

@@ -1,18 +1,12 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-password-field";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-password-field";
import {
canDecryptBackupOnDownload,
getPreferredAgentForDownload,
@@ -27,102 +21,96 @@ import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
@state() private _params?: DownloadDecryptedBackupDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _encryptionKey = "";
@state() private _error = "";
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
this._opened = true;
this._open = true;
this._params = params;
}
public closeDialog() {
this._dialog?.close();
this._open = false;
return true;
}
private _dialogClosed() {
if (this._opened) {
if (this._open) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._open = false;
this._params = undefined;
this._encryptionKey = "";
this._error = "";
}
protected render() {
if (!this._opened || !this._params) {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed} disable-cancel-action>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.title"
)}
</span>
</ha-dialog-header>
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.backup.dialogs.download.title"
)}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
{
download_it_encrypted: html`<button
class="link"
@click=${this._downloadEncrypted}
>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
)}
</button>`,
}
)}
</p>
<div slot="content">
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
{
download_it_encrypted: html`<button
class="link"
@click=${this._downloadEncrypted}
>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
)}
</button>`,
}
)}
</p>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.download.encryption_key"
)}
@input=${this._keyChanged}
></ha-password-field>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.download.encryption_key"
)}
@input=${this._keyChanged}
></ha-password-field>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this._cancel}>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._submit}>
<ha-button slot="primaryAction" @click=${this._submit}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download"
)}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -191,17 +179,8 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
--dialog-content-padding: 8px 24px;
max-width: 500px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
button.link {

View File

@@ -1,15 +1,14 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -24,89 +23,83 @@ import type { ShowBackupEncryptionKeyDialogParams } from "./show-dialog-show-bac
class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
@state() private _open = false;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
this._params = params;
this._open = true;
}
public closeDialog() {
if (this._open) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._open = false;
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
private _closeDialog() {
this._dialog.close();
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.backup.dialogs.show_encryption_key.title"
)}
prevent-scrim-close
@closed=${this.closeDialog}
>
<ha-icon-button
slot="headerNavigationIcon"
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.show_encryption_key.description"
)}
</p>
<div class="encryption-key">
<p>${this._params?.currentKey}</p>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this._closeDialog}
.path=${mdiContentCopy}
@click=${this._copyKeyToClipboard}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.backup.dialogs.show_encryption_key.title"
)}
</span>
</ha-dialog-header>
<div slot="content">
<p>
${this.hass.localize(
"ui.panel.config.backup.dialogs.show_encryption_key.description"
)}
</p>
<div class="encryption-key">
<p>${this._params?.currentKey}</p>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
size="small"
appearance="plain"
slot="end"
@click=${this._download}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
</div>
<div slot="actions">
<ha-button @click=${this._closeDialog}>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button slot="end" @click=${this._download}>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -135,10 +128,8 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
@@ -169,14 +160,7 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
flex: none;
margin: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}

View File

@@ -39,6 +39,7 @@ import {
} from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -163,24 +164,21 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
configSections.dashboard_2,
configSections.dashboard_3,
]);
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => {
const pages: PageNavigation[] = [];
if (isCloudLoaded) {
pages.push({
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
});
}
return [...pages, ...configSections.dashboard];
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -310,21 +308,18 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
: ""}
</ha-card>`
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
).map(
(categoryPages) => html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
)}
></ha-config-navigation>
</ha-card>
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
</ha-top-app-bar-fixed>

View File

@@ -3,17 +3,14 @@ import {
mdiAccount,
mdiBackupRestore,
mdiBadgeAccountHorizontal,
mdiBluetooth,
mdiCellphoneCog,
mdiCog,
mdiDatabase,
mdiDevices,
mdiFlask,
mdiHub,
mdiInformation,
mdiInformationOutline,
mdiLabel,
mdiLan,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
@@ -23,19 +20,15 @@ import {
mdiNfcVariant,
mdiPalette,
mdiPaletteSwatch,
mdiProtocol,
mdiPuzzle,
mdiRobot,
mdiScrewdriver,
mdiScriptText,
mdiShape,
mdiSofa,
mdiStore,
mdiTools,
mdiUpdate,
mdiViewDashboard,
mdiZigbee,
mdiZWave,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -108,72 +101,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiMicrophone,
iconColor: "#3263C3",
},
],
dashboard_2: [
{
path: "/config/zwave_js",
name: "Z-Wave",
iconPath: mdiZWave,
iconColor: "#153163",
component: "zwave_js",
translationKey: "zwave_js",
},
{
path: "/config/zha",
name: "Zigbee",
iconPath: mdiZigbee,
iconColor: "#E74011",
component: "zha",
translationKey: "zha",
},
{
path: "/config/matter",
name: "Matter",
iconPath: mdiHub,
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
},
{
path: "/config/thread",
name: "Thread",
iconPath: mdiProtocol,
iconColor: "#ED7744",
component: "thread",
translationKey: "thread",
},
{
path: "/config/bluetooth",
name: "Bluetooth",
iconPath: mdiBluetooth,
iconColor: "#0082FC",
component: "bluetooth",
translationKey: "bluetooth",
},
{
path: "/knx",
name: "KNX",
iconPath: mdiLan,
iconColor: "#4EAA66",
component: "knx",
translationKey: "knx",
},
{
path: "/insteon",
name: "Insteon",
iconPath: mdiLan,
iconColor: "#E4002C",
component: "insteon",
translationKey: "insteon",
},
{
path: "/hacs",
name: "Home Assistant Community Store",
iconPath: mdiStore,
iconColor: "#41BDF5",
component: "hacs",
translationKey: "hacs",
},
{
path: "/config/tags",
translationKey: "tags",
@@ -181,8 +108,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#616161",
component: "tag",
},
],
dashboard_3: [
{
path: "/config/person",
translationKey: "people",

View File

@@ -1,11 +1,11 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import "../../../components/ha-md-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { HaSwitch } from "../../../components/ha-switch";
@@ -30,13 +30,14 @@ export class DialogLabsPreviewFeatureEnable
@state() private _createBackup = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _open = false;
public async showDialog(
params: LabsPreviewFeatureEnableDialogParams
): Promise<void> {
this._params = params;
this._createBackup = false;
this._open = true;
this._fetchBackupConfig();
if (isComponentLoaded(this.hass, "hassio")) {
this._fetchUpdateBackupConfig();
@@ -44,7 +45,7 @@ export class DialogLabsPreviewFeatureEnable
}
public closeDialog(): boolean {
this._dialog?.close();
this._open = false;
return true;
}
@@ -143,72 +144,69 @@ export class DialogLabsPreviewFeatureEnable
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<span slot="headline">
${this.hass.localize("ui.panel.config.labs.enable_title")}
</span>
<div slot="content">
<p>
${this.hass.localize(
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
</p>
</div>
<div slot="actions">
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div>
<ha-button appearance="plain" @click=${this._handleCancel}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
appearance="filled"
variant="brand"
@click=${this._handleConfirm}
>
${this.hass.localize("ui.panel.config.labs.enable")}
</ha-button>
</div>
</div>
</ha-md-dialog>
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize("ui.panel.config.labs.enable_title")}
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
</p>
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._handleCancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
appearance="filled"
variant="brand"
@click=${this._handleConfirm}
>
${this.hass.localize("ui.panel.config.labs.enable")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
static readonly styles = css`
ha-md-dialog {
--dialog-content-padding: var(--ha-space-6);
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-0);
}
p {
margin: 0;
margin: 0 var(--ha-space-6) var(--ha-space-6);
color: var(--secondary-text-color);
}
div[slot="actions"] {
display: flex;
flex-direction: column;
padding: 0;
}
ha-md-list {
background: none;
--md-list-item-leading-space: var(--ha-space-6);
@@ -217,13 +215,6 @@ export class DialogLabsPreviewFeatureEnable
padding: 0;
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
div[slot="actions"] > div {
display: flex;
justify-content: flex-end;
gap: var(--ha-space-2);
padding: var(--ha-space-4) var(--ha-space-6);
}
`;
}

View File

@@ -1,7 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-md-dialog";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-spinner";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
@@ -39,36 +39,36 @@ export class DialogLabsProgress
}
return html`
<ha-md-dialog
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
disable-cancel-action
prevent-scrim-close
@closed=${this._handleClosed}
>
<div slot="content">
<div class="summary">
<ha-spinner></ha-spinner>
<div class="content">
<p class="heading">
${this.hass.localize(
"ui.panel.config.labs.progress.creating_backup"
)}
</p>
<p class="description">
${this.hass.localize(
this._params.enabled
? "ui.panel.config.labs.progress.backing_up_before_enabling"
: "ui.panel.config.labs.progress.backing_up_before_disabling"
)}
</p>
</div>
<div slot="header"></div>
<div class="summary">
<ha-spinner></ha-spinner>
<div class="content">
<p class="heading">
${this.hass.localize(
"ui.panel.config.labs.progress.creating_backup"
)}
</p>
<p class="description">
${this.hass.localize(
this._params.enabled
? "ui.panel.config.labs.progress.backing_up_before_enabling"
: "ui.panel.config.labs.progress.backing_up_before_disabling"
)}
</p>
</div>
</div>
</ha-md-dialog>
</ha-wa-dialog>
`;
}
static readonly styles = css`
ha-md-dialog {
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-6);
}

View File

@@ -2,7 +2,9 @@ import { mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { navigate } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
@@ -22,7 +24,10 @@ import {
getSummedData,
} from "../../data/energy";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import {
isStrategyView,
type LovelaceViewConfig,
} from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
@@ -33,7 +38,6 @@ import type { ExtraActionItem } from "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import type { LocalizeKeys } from "../../common/translations/localize";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
@@ -48,6 +52,7 @@ const OVERVIEW_VIEW = {
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -56,6 +61,7 @@ const ENERGY_VIEW = {
strategy: {
type: "energy",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -64,6 +70,7 @@ const WATER_VIEW = {
strategy: {
type: "water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -72,6 +79,7 @@ const GAS_VIEW = {
strategy: {
type: "gas",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -223,6 +231,16 @@ class PanelEnergy extends LitElement {
return nothing;
}
const routePath = this.route?.path?.split("/")[1] || "";
const currentView = this._lovelace.config.views.find(
(view) => view.path === routePath
);
const showEnergySelector =
currentView &&
isStrategyView(currentView) &&
currentView.strategy?.show_period_selector;
return html`
<hui-root
.hass=${this.hass}
@@ -232,7 +250,20 @@ class PanelEnergy extends LitElement {
.panel=${this.panel}
.extraActionItems=${this._extraActionItems}
@reload-energy-panel=${this._reloadConfig}
></hui-root>
class=${classMap({ "has-period-selector": showEnergySelector })}
>
</hui-root>
${showEnergySelector
? html`
<ha-card raised class="period-selector">
<hui-energy-period-selector
.hass=${this.hass}
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
vertical-opening-direction="up"
></hui-energy-period-selector>
</ha-card>
`
: nothing}
`;
}
@@ -631,24 +662,6 @@ class PanelEnergy extends LitElement {
-webkit-user-select: none;
-moz-user-select: none;
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
.centered {
width: 100%;
height: 100%;
@@ -656,6 +669,28 @@ class PanelEnergy extends LitElement {
align-items: center;
justify-content: center;
}
hui-root.has-period-selector {
--view-container-padding-bottom: var(--ha-space-16);
}
.period-selector {
position: fixed;
bottom: calc(var(--ha-space-2) + var(--safe-area-inset-bottom, 0px));
left: var(--mdc-drawer-width, 0);
right: var(--safe-area-inset-right, 0px);
inset-inline-start: var(--mdc-drawer-width, 0);
inset-inline-end: var(--safe-area-inset-right, 0px);
margin: var(--ha-space-2) auto;
max-width: calc(min(450px, 100% - var(--ha-space-4)));
box-sizing: border-box;
padding-left: calc(
var(--ha-space-4) + var(--safe-area-inset-left, 0px)
);
padding-right: 0;
padding-inline-start: calc(
var(--ha-space-4) + var(--safe-area-inset-left, 0px)
);
padding-inline-end: 0;
}
`,
];
}

View File

@@ -5,7 +5,6 @@ import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
@customElement("energy-overview-view-strategy")
@@ -64,24 +63,20 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
(source.type === "grid" && source.power?.length)
);
const overviewSection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "energy-date-selection",
collection_key: collectionKey,
allow_compare: false,
},
],
};
if (hasGrid || hasBattery || hasSolar) {
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution",
collection_key: collectionKey,
view.sections!.push({
type: "grid",
cards: [
{
title: hass.localize(
"ui.panel.energy.cards.energy_distribution_title"
),
type: "energy-distribution",
collection_key: collectionKey,
},
],
});
}
view.sections!.push(overviewSection);
if (prefs.energy_sources.length) {
view.sections!.push({

View File

@@ -53,10 +53,6 @@ export class EnergyViewStrategy extends ReactiveElement {
(d) => d.included_in_stat
);
view.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
view.cards!.push({
type: "energy-compare",
collection_key: "energy_dashboard",

View File

@@ -40,10 +40,6 @@ export class GasViewStrategy extends ReactiveElement {
const section = view.sections![0] as LovelaceSectionConfig;
section.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
section.cards!.push({
type: "energy-compare",
collection_key: collectionKey,

View File

@@ -41,10 +41,6 @@ export class WaterViewStrategy extends ReactiveElement {
const section = view.sections![0] as LovelaceSectionConfig;
section.cards!.push({
type: "energy-date-selection",
collection_key: collectionKey,
});
section.cards!.push({
type: "energy-compare",
collection_key: collectionKey,

View File

@@ -17,6 +17,7 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { classMap } from "lit/directives/class-map";
import {
calcDate,
calcDateProperty,
@@ -69,6 +70,11 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean, attribute: "allow-compare" }) public allowCompare =
true;
@property({ attribute: "vertical-opening-direction" })
public verticalOpeningDirection?: "up" | "down";
@state() _datepickerOpen = false;
@state() _startDate?: Date;
@state() _endDate?: Date;
@@ -150,7 +156,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
);
return html`
<div class="row">
<div
class=${classMap({
row: true,
"datepicker-open": this._datepickerOpen,
})}
>
<div class="backdrop"></div>
<div class="label">
${simpleRange === "day"
? this.narrow
@@ -202,8 +214,10 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
.ranges=${this._ranges}
@value-changed=${this._dateRangeChanged}
@preset-selected=${this._presetSelected}
@toggle=${this._handleDatepickerToggle}
minimal
header-position
.verticalOpeningDirection=${this.verticalOpeningDirection}
></ha-date-range-picker>
</div>
@@ -446,6 +460,10 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
this._endDate = energyData.end || endOfToday();
}
private _handleDatepickerToggle(ev: CustomEvent<{ open: boolean }>) {
this._datepickerOpen = ev.detail.open;
}
private _toggleCompare(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "interaction") {
return;
@@ -496,6 +514,29 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
flex-shrink: 0;
--ha-button-theme-color: currentColor;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: var(--dialog-z-index, 8);
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter)
);
backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter)
);
pointer-events: none;
opacity: 0;
transition: opacity var(--ha-animation-base-duration) ease-in-out;
}
.datepicker-open .backdrop {
opacity: 1;
pointer-events: auto;
}
`;
}

View File

@@ -26,6 +26,7 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
import { fireEvent } from "../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { goBack, navigate } from "../../common/navigate";
@@ -37,8 +38,8 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { isMobileClient } from "../../util/is_mobile";
import { afterNextRender } from "../../common/util/render-status";
import { withViewTransition } from "../../common/util/view-transition";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-icon";
@@ -81,6 +82,7 @@ import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isMobileClient } from "../../util/is_mobile";
import { showToast } from "../../util/toast";
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../config/automation/show-dialog-new-automation";
@@ -98,8 +100,6 @@ import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
import { withViewTransition } from "../../common/util/view-transition";
interface ActionItem {
icon: string;
@@ -1546,7 +1546,10 @@ class HUIRoot extends LitElement {
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
padding-bottom: calc(
var(--safe-area-inset-bottom) +
var(--view-container-padding-bottom, 0px)
);
}
.narrow hui-view-container {
padding-left: var(--safe-area-inset-left);

View File

@@ -48,6 +48,9 @@ export const mainStyles = css`
*/
--safe-width: calc(100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right));
--safe-height: calc(100vh - var(--safe-area-inset-top) - var(--safe-area-inset-bottom));
/* dialog backdrop filter */
--ha-dialog-scrim-backdrop-filter: brightness(68%);
}
`;

View File

@@ -2308,30 +2308,6 @@
},
"cloud": {
"secondary": "Loading..."
},
"zwave_js": {
"secondary": "Manage your Z-Wave network"
},
"zha": {
"secondary": "Manage your Zigbee network"
},
"matter": {
"secondary": "Manage your Matter network"
},
"thread": {
"secondary": "Manage your Thread network"
},
"bluetooth": {
"secondary": "Manage your Bluetooth devices"
},
"knx": {
"secondary": "Manage your KNX network"
},
"insteon": {
"secondary": "Manage your Insteon network"
},
"hacs": {
"secondary": "Manage community integrations and frontend modules"
}
},
"common": {