1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Light dashboard toggle area (#29363)

* Add toggle lights button on light dashboard

* Use not

* Use group card on desktop
This commit is contained in:
Paul Bottein
2026-02-13 08:16:13 +01:00
committed by GitHub
parent b256fc820d
commit a45ef6e019
7 changed files with 261 additions and 15 deletions

View File

@@ -12,6 +12,11 @@ import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/sec
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../../lovelace/strategies/helpers/screen-conditions";
import type { ToggleGroupCardConfig } from "../../lovelace/cards/types";
export interface LightViewStrategyConfig {
type: "light";
@@ -45,6 +50,16 @@ const processAreasForLight = (
}
if (areaCards.length > 0) {
// Visibility condition: any light is on
const anyOnCondition = {
condition: "or" as const,
conditions: areaLights.map((entityId) => ({
condition: "state" as const,
entity: entityId,
state: "on",
})),
};
cards.push({
heading_style: "subtitle",
type: "heading",
@@ -53,7 +68,56 @@ const processAreasForLight = (
action: "navigate",
navigation_path: `/home/areas-${area.area_id}`,
},
badges: [
// Toggle buttons for mobile
{
type: "button",
icon: "mdi:power",
tap_action: {
action: "perform-action",
perform_action: "light.turn_on",
target: {
area_id: area.area_id,
},
},
visibility: [
SMALL_SCREEN_CONDITION,
{
condition: "not",
conditions: [anyOnCondition],
},
],
},
{
type: "button",
icon: "mdi:power",
color: "amber",
tap_action: {
action: "perform-action",
perform_action: "light.turn_off",
target: {
area_id: area.area_id,
},
},
visibility: [SMALL_SCREEN_CONDITION, anyOnCondition],
},
] satisfies LovelaceCardConfig[],
});
// Toggle group card for desktop
cards.push({
type: "toggle-group",
title: hass.localize("ui.panel.lovelace.strategy.light.all_lights"),
color: "amber",
entities: areaLights,
visibility: [LARGE_SCREEN_CONDITION],
grid_options: {
columns: 6,
rows: 1,
min_columns: 6,
},
} as ToggleGroupCardConfig);
cards.push(...areaCards);
}
}

View File

@@ -0,0 +1,161 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import { forwardHaptic } from "../../../data/haptics";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { ToggleGroupCardConfig } from "./types";
@customElement("hui-toggle-group-card")
export class HuiToggleGroupCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ToggleGroupCardConfig;
public setConfig(config: ToggleGroupCardConfig): void {
if (!config.entities?.length) {
throw new Error("Entities are required");
}
this._config = config;
}
public getCardSize(): number {
return this._config?.vertical ? 2 : 1;
}
public getGridOptions(): LovelaceGridOptions {
const columns = 6;
let rows = 1;
let min_columns = 6;
if (this._config?.vertical) {
rows++;
min_columns = 3;
}
return {
columns,
rows,
min_columns,
min_rows: rows,
};
}
private _getOnEntities(): HassEntity[] {
if (!this.hass || !this._config) return [];
return this._config.entities
.map((entityId) => this.hass!.states[entityId])
.filter(
(stateObj): stateObj is HassEntity =>
stateObj !== undefined && stateActive(stateObj)
);
}
private _computeColor(): string | undefined {
if (!this._config || !this.hass) return undefined;
const onEntities = this._getOnEntities();
if (onEntities.length === 0) return undefined;
if (this._config.color) {
return computeCssColor(this._config.color);
}
return stateColorCss(onEntities[0]);
}
private _computeSecondary(): string {
if (!this.hass || !this._config) return "";
const onCount = this._getOnEntities().length;
const total = this._config.entities.length;
if (onCount === 0) {
return this.hass.localize("ui.card.toggle-group.all_off");
}
if (onCount === total) {
return this.hass.localize("ui.card.toggle-group.all_on");
}
return this.hass.localize("ui.card.toggle-group.some_on", {
on_count: onCount,
total_count: total,
});
}
private _handleTap(): void {
if (!this.hass || !this._config) return;
const onEntities = this._getOnEntities();
const domain = computeDomain(this._config.entities[0]);
let service: string;
if (domain === "cover") {
service = onEntities.length > 0 ? "close_cover" : "open_cover";
} else {
service = onEntities.length > 0 ? "turn_off" : "turn_on";
}
this.hass.callService(domain, service, {
entity_id: this._config.entities,
});
forwardHaptic(this, "light");
}
protected render() {
if (!this._config || !this.hass) {
return nothing;
}
const color = this._computeColor();
const style = {
"--tile-color": color,
};
return html`
<ha-card style=${styleMap(style)}>
<ha-tile-container .vertical=${Boolean(this._config.vertical)}>
<ha-tile-icon
slot="icon"
icon="mdi:power"
.interactive=${true}
@action=${this._handleTap}
></ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${this._config.title}
.secondary=${this._computeSecondary()}
></ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
}
ha-card {
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
box-shadow: none;
height: 100%;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-toggle-group-card": HuiToggleGroupCard;
}
}

View File

@@ -665,6 +665,13 @@ export interface HomeSummaryCard extends LovelaceCardConfig {
double_tap_action?: ActionConfig;
}
export interface ToggleGroupCardConfig extends LovelaceCardConfig {
title: string;
entities: string[];
color?: string;
vertical?: boolean;
}
export interface DistributionEntityConfig extends EntityConfig {
color?: string;
}

View File

@@ -93,6 +93,7 @@ const LAZY_LOAD_TYPES = {
picture: () => import("../cards/hui-picture-card"),
"plant-status": () => import("../cards/hui-plant-status-card"),
"recovery-mode": () => import("../cards/hui-recovery-mode-card"),
"toggle-group": () => import("../cards/hui-toggle-group-card"),
"todo-list": () => import("../cards/hui-todo-list-card"),
"shopping-list": () => import("../cards/hui-shopping-list-card"),
starting: () => import("../cards/hui-starting-card"),

View File

@@ -0,0 +1,14 @@
import type {
Condition,
ScreenCondition,
} from "../../common/validate-condition";
export const LARGE_SCREEN_CONDITION: ScreenCondition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
export const SMALL_SCREEN_CONDITION: Condition = {
condition: "not",
conditions: [LARGE_SCREEN_CONDITION],
};

View File

@@ -27,7 +27,10 @@ import type {
TileCardConfig,
UpdatesCardConfig,
} from "../../cards/types";
import type { Condition } from "../../common/validate-condition";
import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../helpers/screen-conditions";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
@@ -84,16 +87,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const largeScreenCondition: Condition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
const smallScreenCondition: Condition = {
condition: "screen",
media_query: "(max-width: 870px)",
};
const otherDevicesFilters = OTHER_DEVICES_FILTERS.map((filter) =>
generateEntityFilter(hass, filter)
);
@@ -202,7 +195,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.favorites"),
heading_style: "title",
visibility: [largeScreenCondition],
visibility: [LARGE_SCREEN_CONDITION],
grid_options: {
rows: "auto", // Compact style
},
@@ -361,7 +354,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
? {
type: "grid",
column_span: maxColumns,
visibility: [smallScreenCondition],
visibility: [SMALL_SCREEN_CONDITION],
cards: [summaryHeadingCard, ...mobileSummaryCards],
}
: undefined;
@@ -460,7 +453,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
sidebar_label: hass.localize(
"ui.panel.lovelace.strategy.home.summaries"
),
visibility: [largeScreenCondition],
visibility: [LARGE_SCREEN_CONDITION],
},
}),
};

View File

@@ -215,6 +215,11 @@
"no_media_playing": "No media playing",
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}"
},
"toggle-group": {
"all_off": "All off",
"all_on": "All on",
"some_on": "{on_count} of {total_count} on"
},
"discovered-devices": {
"title": "Devices discovered",
"count_devices": "{count} {count, plural,\n one {device to add}\n other {devices to add}\n}",
@@ -7943,7 +7948,8 @@
},
"light": {
"lights": "Lights",
"other_lights": "Other lights"
"other_lights": "Other lights",
"all_lights": "All lights"
},
"security": {
"devices": "Devices",