diff --git a/src/panels/home/ha-panel-home.ts b/src/panels/home/ha-panel-home.ts
index 28379d7307..d9a0031287 100644
--- a/src/panels/home/ha-panel-home.ts
+++ b/src/panels/home/ha-panel-home.ts
@@ -15,12 +15,13 @@ import {
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
+import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
+import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail";
+import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
import "../lovelace/hui-root";
import type { ExtraActionItem } from "../lovelace/hui-root";
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
-import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
-import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
@customElement("ha-panel-home")
@@ -172,13 +173,26 @@ class PanelHome extends LitElement {
private _handleLLCustomEvent = (ev: Event) => {
const detail = (ev as CustomEvent).detail;
if (detail.home_panel) {
- const { type, device_id } = detail.home_panel;
- if (type === "assign_area") {
- this._showAssignAreaDialog(device_id);
+ const { type } = detail.home_panel;
+ switch (type) {
+ case "assign_area": {
+ const { device_id } = detail.home_panel;
+ this._showAssignAreaDialog(device_id);
+ break;
+ }
+ case "add_integration": {
+ this._showAddIntegrationDialog();
+ break;
+ }
}
}
};
+ private async _showAddIntegrationDialog() {
+ await this.hass.loadFragmentTranslation("config");
+ showAddIntegrationDialog(this, { navigateToResult: false });
+ }
+
private _showAssignAreaDialog(deviceId: string) {
const device = this.hass.devices[deviceId];
if (!device) {
diff --git a/src/panels/lovelace/cards/hui-empty-state-card.ts b/src/panels/lovelace/cards/hui-empty-state-card.ts
index b9a050690a..38a35ec48b 100644
--- a/src/panels/lovelace/cards/hui-empty-state-card.ts
+++ b/src/panels/lovelace/cards/hui-empty-state-card.ts
@@ -1,6 +1,9 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
+import { styleMap } from "lit/directives/style-map";
+import { ifDefined } from "lit/directives/if-defined";
+import { computeCssColor } from "../../../common/color/compute-color";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon";
@@ -49,17 +52,44 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
>
${this._config.icon
- ? html`
`
+ ? html`
+
+ `
: nothing}
${this._config.title ? html`
${this._config.title}
` : nothing}
${this._config.content
? html`
${this._config.content}
`
: nothing}
- ${this._config.tap_action && this._config.action_button_text
+ ${this._config.buttons?.length
? html`
-
- ${this._config.action_button_text}
-
+
+ ${this._config.buttons.map(
+ (button, index) => html`
+
+ ${button.icon
+ ? html``
+ : nothing}
+ ${button.text}
+
+ `
+ )}
+
`
: nothing}
@@ -67,9 +97,11 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
`;
}
- private _handleAction(): void {
- if (this._config?.tap_action && this.hass) {
- handleAction(this, this.hass, this._config, "tap");
+ private _handleButtonAction(ev: Event): void {
+ const index = (ev.currentTarget as any).index;
+ const button = this._config?.buttons?.[index];
+ if (this.hass && button) {
+ handleAction(this, this.hass, { tap_action: button.tap_action }, "tap");
}
}
@@ -94,8 +126,8 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
max-width: 640px;
margin: 0 auto;
}
- ha-icon {
- --mdc-icon-size: var(--ha-space-12);
+ .card-icon {
+ --mdc-icon-size: var(--ha-space-16);
color: var(--secondary-text-color);
}
h1 {
@@ -107,6 +139,12 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
margin: 0;
color: var(--secondary-text-color);
}
+ .buttons {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: var(--ha-space-2);
+ }
.content-only {
background: none;
box-shadow: none;
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index 0afc347212..316dcdbc0d 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -57,13 +57,21 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
conditions: (Condition | LegacyCondition)[];
}
+export interface EmptyStateButtonConfig {
+ text: string;
+ icon?: string;
+ appearance?: "accent" | "filled" | "outlined" | "plain";
+ variant?: "brand" | "neutral" | "success" | "warning" | "danger";
+ tap_action: ActionConfig;
+}
+
export interface EmptyStateCardConfig extends LovelaceCardConfig {
content_only?: boolean;
icon?: string;
+ icon_color?: string;
title?: string;
content?: string;
- action_button_text?: string;
- tap_action?: ActionConfig;
+ buttons?: EmptyStateButtonConfig[];
}
export interface EntityCardConfig extends LovelaceCardConfig {
diff --git a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts
index e42ea6c46b..0b74aa2db5 100644
--- a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts
@@ -1,8 +1,16 @@
-import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
-import { assert, assign, boolean, object, optional, string } from "superstruct";
+import {
+ array,
+ assert,
+ assign,
+ boolean,
+ enums,
+ object,
+ optional,
+ string,
+} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -16,15 +24,25 @@ import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
+const buttonStruct = object({
+ text: string(),
+ icon: optional(string()),
+ appearance: optional(enums(["accent", "filled", "outlined", "plain"])),
+ variant: optional(
+ enums(["brand", "neutral", "success", "warning", "danger"])
+ ),
+ tap_action: actionConfigStruct,
+});
+
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
content_only: optional(boolean()),
icon: optional(string()),
+ icon_color: optional(string()),
title: optional(string()),
content: optional(string()),
- action_button_text: optional(string()),
- tap_action: optional(actionConfigStruct),
+ buttons: optional(array(buttonStruct)),
})
);
@@ -70,24 +88,66 @@ export class HuiEmptyStateCardEditor
},
},
{ name: "icon", selector: { icon: {} } },
+ {
+ name: "icon_color",
+ selector: {
+ ui_color: {},
+ },
+ },
{ name: "title", selector: { text: {} } },
{ name: "content", selector: { text: { multiline: true } } },
{
- name: "interactions",
- type: "expandable",
- flatten: true,
- iconPath: mdiGestureTap,
- schema: [
- { name: "action_button_text", selector: { text: {} } },
- {
- name: "tap_action",
- selector: {
- ui_action: {
- default_action: "none",
+ name: "buttons",
+ selector: {
+ object: {
+ multiple: true,
+ label_field: "text",
+ fields: {
+ text: {
+ selector: { text: {} },
+ required: true,
+ },
+ icon: {
+ selector: { icon: {} },
+ },
+ appearance: {
+ selector: {
+ select: {
+ options: [
+ { value: "accent", label: "Accent" },
+ { value: "filled", label: "Filled" },
+ { value: "outlined", label: "Outlined" },
+ { value: "plain", label: "Plain" },
+ ],
+ mode: "dropdown",
+ },
+ },
+ },
+ variant: {
+ selector: {
+ select: {
+ options: [
+ { value: "brand", label: "Brand" },
+ { value: "neutral", label: "Neutral" },
+ { value: "success", label: "Success" },
+ { value: "warning", label: "Warning" },
+ { value: "danger", label: "Danger" },
+ ],
+ mode: "dropdown",
+ },
+ },
+ },
+ tap_action: {
+ selector: {
+ ui_action: {
+ default_action: "none",
+ },
+ },
+ required: true,
},
},
},
- ],
+ },
},
] as const satisfies readonly HaFormSchema[]
);
@@ -134,7 +194,7 @@ export class HuiEmptyStateCardEditor
switch (schema.name) {
case "style":
case "content":
- case "action_button_text":
+ case "buttons":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
);
diff --git a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts
index 78b0a579f1..dd3c2850ec 100644
--- a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts
+++ b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts
@@ -27,6 +27,7 @@ import {
export interface HomeAreaViewStrategyConfig {
type: "home-area";
area?: string;
+ home_panel?: boolean;
}
@customElement("home-area-view-strategy")
@@ -365,13 +366,46 @@ export class HomeAreaViewStrategy extends ReactiveElement {
{
type: "empty-state",
icon: "mdi:sofa-outline",
+ icon_color: "primary",
content_only: true,
title: hass.localize(
- "ui.panel.lovelace.strategy.areas.empty_state_title"
+ "ui.panel.lovelace.strategy.home-area.no_devices_title"
),
content: hass.localize(
- "ui.panel.lovelace.strategy.areas.empty_state_content"
+ "ui.panel.lovelace.strategy.home-area.no_devices_content"
),
+ ...(config.home_panel && hass.user?.is_admin
+ ? {
+ buttons: [
+ {
+ icon: "mdi:plus",
+ text: hass.localize(
+ "ui.panel.lovelace.strategy.home-area.no_devices_add_device"
+ ),
+ appearance: "plain",
+ variant: "brand",
+ tap_action: {
+ action: "fire-dom-event",
+ home_panel: {
+ type: "add_integration",
+ },
+ },
+ },
+ {
+ icon: "mdi:home-plus",
+ text: hass.localize(
+ "ui.panel.lovelace.strategy.home-area.no_devices_assign_device"
+ ),
+ appearance: "plain",
+ variant: "brand",
+ tap_action: {
+ action: "navigate",
+ navigation_path: "/home/other-devices",
+ },
+ },
+ ],
+ }
+ : {}),
} as EmptyStateCardConfig,
],
};
diff --git a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts
index 1e2cc10876..c6b5fc3b1c 100644
--- a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts
+++ b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts
@@ -59,6 +59,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
strategy: {
type: "home-area",
area: area.area_id,
+ home_panel: config.home_panel,
} satisfies HomeAreaViewStrategyConfig,
};
});
@@ -92,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
strategy: {
type: "home-overview",
favorite_entities: config.favorite_entities,
+ home_panel: config.home_panel,
} satisfies HomeOverviewViewStrategyConfig,
},
...areaViews,
diff --git a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts
index 0f5e9da8e8..e0d1b2d10e 100644
--- a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts
+++ b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts
@@ -142,7 +142,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
type: "button",
icon: "mdi:home-plus",
text: hass.localize(
- "ui.panel.lovelace.strategy.other_devices.assign_area"
+ "ui.panel.lovelace.strategy.home-other-devices.assign_area"
),
tap_action: {
action: "fire-dom-event",
@@ -178,7 +178,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
{
type: "heading",
heading: hass.localize(
- "ui.panel.lovelace.strategy.other_devices.helpers"
+ "ui.panel.lovelace.strategy.home-other-devices.helpers"
),
} satisfies HeadingCardConfig,
...helpersEntities.map((e) => ({
@@ -197,7 +197,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
{
type: "heading",
heading: hass.localize(
- "ui.panel.lovelace.strategy.other_devices.entities"
+ "ui.panel.lovelace.strategy.home-other-devices.entities"
),
} satisfies HeadingCardConfig,
...otherEntities.map((e) => ({
@@ -216,12 +216,13 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
{
type: "empty-state",
icon: "mdi:check-all",
+ icon_color: "primary",
content_only: true,
title: hass.localize(
- "ui.panel.lovelace.strategy.other_devices.empty_state_title"
+ "ui.panel.lovelace.strategy.home-other-devices.all_organized_title"
),
content: hass.localize(
- "ui.panel.lovelace.strategy.other_devices.empty_state_content"
+ "ui.panel.lovelace.strategy.home-other-devices.all_organized_content"
),
} as EmptyStateCardConfig,
],
diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
index 92db96e5ad..3503c4f451 100644
--- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
+++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
@@ -20,6 +20,7 @@ import type { HomeAssistant } from "../../../../types";
import type {
AreaCardConfig,
DiscoveredDevicesCardConfig,
+ EmptyStateCardConfig,
HomeSummaryCard,
MarkdownCardConfig,
TileCardConfig,
@@ -32,6 +33,7 @@ import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
favorite_entities?: string[];
+ home_panel?: boolean;
}
const computeAreaCard = (
@@ -341,6 +343,59 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
}
: undefined;
+ // No sections, show empty state
+ if (floorsSections.length === 0) {
+ return {
+ type: "panel",
+ cards: [
+ {
+ type: "empty-state",
+ icon: "mdi:home-assistant",
+ icon_color: "primary",
+ content_only: true,
+ title: hass.localize(
+ "ui.panel.lovelace.strategy.home.welcome_title"
+ ),
+ content: hass.localize(
+ "ui.panel.lovelace.strategy.home.welcome_content"
+ ),
+ ...(config.home_panel && hass.user?.is_admin
+ ? {
+ buttons: [
+ {
+ icon: "mdi:plus",
+ text: hass.localize(
+ "ui.panel.lovelace.strategy.home.welcome_add_device"
+ ),
+ appearance: "filled",
+ variant: "brand",
+ tap_action: {
+ action: "fire-dom-event",
+ home_panel: {
+ type: "add_integration",
+ },
+ },
+ },
+ {
+ icon: "mdi:home-edit",
+ text: hass.localize(
+ "ui.panel.lovelace.strategy.home.welcome_edit_areas"
+ ),
+ appearance: "plain",
+ variant: "brand",
+ tap_action: {
+ action: "navigate",
+ navigation_path: "/config/areas/dashboard",
+ },
+ },
+ ],
+ }
+ : {}),
+ } as EmptyStateCardConfig,
+ ],
+ };
+ }
+
const sections = (
[favoritesSection, mobileSummarySection, ...floorsSections] satisfies (
| LovelaceSectionRawConfig
diff --git a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
index c29e8be4ec..b79c38f7b1 100644
--- a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
+++ b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts
@@ -71,6 +71,7 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
{
type: "empty-state",
icon: "mdi:home-assistant",
+ icon_color: "primary",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_title"
@@ -80,13 +81,19 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
),
...(hass.user?.is_admin
? {
- action_button_text: hass.localize(
- "ui.panel.lovelace.strategy.original-states.empty_state_action"
- ),
- tap_action: {
- action: "navigate",
- navigation_path: "/config/integrations/dashboard",
- },
+ buttons: [
+ {
+ text: hass.localize(
+ "ui.panel.lovelace.strategy.original-states.empty_state_action"
+ ),
+ appearance: "filled",
+ variant: "brand",
+ tap_action: {
+ action: "navigate",
+ navigation_path: "/config/integrations/dashboard",
+ },
+ },
+ ],
}
: {}),
} as EmptyStateCardConfig,
diff --git a/src/translations/en.json b/src/translations/en.json
index 9b67bb48fa..3e5f77b146 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -7524,8 +7524,6 @@
"empty_state_action": "Go to the integrations page"
},
"areas": {
- "empty_state_title": "No devices",
- "empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.",
"sensors": "Sensors",
"sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
"edit_the_area": "edit the area",
@@ -7559,7 +7557,11 @@
"automations": "Automations",
"for_you": "For you",
"home": "Home",
- "favorites": "Favorites"
+ "favorites": "Favorites",
+ "welcome_title": "No devices here yet",
+ "welcome_content": "Add lights, switches, sensors, or other smart home devices to get started.",
+ "welcome_add_device": "Add new device",
+ "welcome_edit_areas": "Edit areas"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
@@ -7581,12 +7583,18 @@
"media_players": "Media players",
"other_media_players": "Other media players"
},
- "other_devices": {
+ "home-other-devices": {
"helpers": "Helpers",
"entities": "Entities",
"assign_area": "Assign area",
- "empty_state_title": "All devices are organized",
- "empty_state_content": "There are no unassigned devices left. All devices are organized into areas."
+ "all_organized_title": "All devices are organized",
+ "all_organized_content": "There are no unassigned devices left. All devices are organized into areas."
+ },
+ "home-area": {
+ "no_devices_title": "This is a blank canvas",
+ "no_devices_content": "Add your smart lights, switches, or sensors to this area to get started.",
+ "no_devices_add_device": "Add new device",
+ "no_devices_assign_device": "Assign existing device"
}
},
"cards": {
@@ -8283,14 +8291,14 @@
},
"empty_state": {
"name": "Empty state",
- "description": "The Empty state card displays a centered message with an optional icon and action button.",
+ "description": "The Empty state card displays a centered message with an optional icon and action buttons.",
"style": "Style",
"style_options": {
"card": "Card",
"content-only": "Content only"
},
"content": "Content",
- "action_text": "Action button text"
+ "buttons": "Buttons"
},
"button": {
"name": "Button",