Skip to content

Commit 7048c5f

Browse files
piitayawendevlinCopilot
authored
Add entity-first card picker for dashboard (#51651)
Co-authored-by: Wendelin <w@pe8.at> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 9ed47be commit 7048c5f

17 files changed

Lines changed: 2675 additions & 138 deletions
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { HomeAssistant } from "../../../types";
2+
import { supportsAlarmModesCardFeature } from "./hui-alarm-modes-card-feature";
3+
import { supportsAreaControlsCardFeature } from "./hui-area-controls-card-feature";
4+
import { supportsBarGaugeCardFeature } from "./hui-bar-gauge-card-feature";
5+
import { supportsButtonCardFeature } from "./hui-button-card-feature";
6+
import { supportsClimateFanModesCardFeature } from "./hui-climate-fan-modes-card-feature";
7+
import { supportsClimateHvacModesCardFeature } from "./hui-climate-hvac-modes-card-feature";
8+
import { supportsClimatePresetModesCardFeature } from "./hui-climate-preset-modes-card-feature";
9+
import { supportsClimateSwingHorizontalModesCardFeature } from "./hui-climate-swing-horizontal-modes-card-feature";
10+
import { supportsClimateSwingModesCardFeature } from "./hui-climate-swing-modes-card-feature";
11+
import { supportsCounterActionsCardFeature } from "./hui-counter-actions-card-feature";
12+
import { supportsCoverOpenCloseCardFeature } from "./hui-cover-open-close-card-feature";
13+
import { supportsCoverPositionFavoriteCardFeature } from "./hui-cover-position-favorite-card-feature";
14+
import { supportsCoverPositionCardFeature } from "./hui-cover-position-card-feature";
15+
import { supportsCoverTiltCardFeature } from "./hui-cover-tilt-card-feature";
16+
import { supportsCoverTiltFavoriteCardFeature } from "./hui-cover-tilt-favorite-card-feature";
17+
import { supportsCoverTiltPositionCardFeature } from "./hui-cover-tilt-position-card-feature";
18+
import { supportsDateSetCardFeature } from "./hui-date-set-card-feature";
19+
import { supportsFanDirectionCardFeature } from "./hui-fan-direction-card-feature";
20+
import { supportsFanOscilatteCardFeature } from "./hui-fan-oscillate-card-feature";
21+
import { supportsFanPresetModesCardFeature } from "./hui-fan-preset-modes-card-feature";
22+
import { supportsFanSpeedCardFeature } from "./hui-fan-speed-card-feature";
23+
import { supportsHumidifierModesCardFeature } from "./hui-humidifier-modes-card-feature";
24+
import { supportsHumidifierToggleCardFeature } from "./hui-humidifier-toggle-card-feature";
25+
import { supportsLawnMowerCommandCardFeature } from "./hui-lawn-mower-commands-card-feature";
26+
import { supportsLightBrightnessCardFeature } from "./hui-light-brightness-card-feature";
27+
import { supportsLightColorFavoritesCardFeature } from "./hui-light-color-favorites-card-feature";
28+
import { supportsLightColorTempCardFeature } from "./hui-light-color-temp-card-feature";
29+
import { supportsLockCommandsCardFeature } from "./hui-lock-commands-card-feature";
30+
import { supportsLockOpenDoorCardFeature } from "./hui-lock-open-door-card-feature";
31+
import { supportsMediaPlayerPlaybackCardFeature } from "./hui-media-player-playback-card-feature";
32+
import { supportsMediaPlayerSoundModeCardFeature } from "./hui-media-player-sound-mode-card-feature";
33+
import { supportsMediaPlayerSourceCardFeature } from "./hui-media-player-source-card-feature";
34+
import { supportsMediaPlayerVolumeButtonsCardFeature } from "./hui-media-player-volume-buttons-card-feature";
35+
import { supportsMediaPlayerVolumeSliderCardFeature } from "./hui-media-player-volume-slider-card-feature";
36+
import { supportsNumericInputCardFeature } from "./hui-numeric-input-card-feature";
37+
import { supportsSelectOptionsCardFeature } from "./hui-select-options-card-feature";
38+
import { supportsTargetHumidityCardFeature } from "./hui-target-humidity-card-feature";
39+
import { supportsTargetTemperatureCardFeature } from "./hui-target-temperature-card-feature";
40+
import { supportsToggleCardFeature } from "./hui-toggle-card-feature";
41+
import { supportsTrendGraphCardFeature } from "./hui-trend-graph-card-feature";
42+
import { supportsUpdateActionsCardFeature } from "./hui-update-actions-card-feature";
43+
import { supportsVacuumCommandsCardFeature } from "./hui-vacuum-commands-card-feature";
44+
import { supportsValveOpenCloseCardFeature } from "./hui-valve-open-close-card-feature";
45+
import { supportsValvePositionFavoriteCardFeature } from "./hui-valve-position-favorite-card-feature";
46+
import { supportsValvePositionCardFeature } from "./hui-valve-position-card-feature";
47+
import { supportsWaterHeaterOperationModesCardFeature } from "./hui-water-heater-operation-modes-card-feature";
48+
import type {
49+
LovelaceCardFeatureConfig,
50+
LovelaceCardFeatureContext,
51+
} from "./types";
52+
53+
export type FeatureType = LovelaceCardFeatureConfig["type"];
54+
55+
export type SupportsFeature = (
56+
hass: HomeAssistant,
57+
context: LovelaceCardFeatureContext
58+
) => boolean;
59+
60+
export const UI_FEATURE_TYPES = [
61+
"alarm-modes",
62+
"area-controls",
63+
"bar-gauge",
64+
"button",
65+
"climate-fan-modes",
66+
"climate-hvac-modes",
67+
"climate-preset-modes",
68+
"climate-swing-modes",
69+
"climate-swing-horizontal-modes",
70+
"counter-actions",
71+
"cover-open-close",
72+
"cover-position-favorite",
73+
"cover-position",
74+
"cover-tilt-favorite",
75+
"cover-tilt-position",
76+
"cover-tilt",
77+
"date-set",
78+
"fan-direction",
79+
"fan-oscillate",
80+
"fan-preset-modes",
81+
"fan-speed",
82+
"humidifier-modes",
83+
"humidifier-toggle",
84+
"lawn-mower-commands",
85+
"light-brightness",
86+
"light-color-temp",
87+
"light-color-favorites",
88+
"lock-commands",
89+
"lock-open-door",
90+
"media-player-playback",
91+
"media-player-sound-mode",
92+
"media-player-source",
93+
"media-player-volume-buttons",
94+
"media-player-volume-slider",
95+
"numeric-input",
96+
"select-options",
97+
"trend-graph",
98+
"target-humidity",
99+
"target-temperature",
100+
"toggle",
101+
"update-actions",
102+
"vacuum-commands",
103+
"valve-open-close",
104+
"valve-position-favorite",
105+
"valve-position",
106+
"water-heater-operation-modes",
107+
] as const satisfies readonly FeatureType[];
108+
109+
export type UiFeatureType = (typeof UI_FEATURE_TYPES)[number];
110+
111+
export const SUPPORTS_FEATURE_TYPES: Record<UiFeatureType, SupportsFeature> = {
112+
"alarm-modes": supportsAlarmModesCardFeature,
113+
"area-controls": supportsAreaControlsCardFeature,
114+
"bar-gauge": supportsBarGaugeCardFeature,
115+
button: supportsButtonCardFeature,
116+
"climate-fan-modes": supportsClimateFanModesCardFeature,
117+
"climate-swing-modes": supportsClimateSwingModesCardFeature,
118+
"climate-swing-horizontal-modes":
119+
supportsClimateSwingHorizontalModesCardFeature,
120+
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
121+
"climate-preset-modes": supportsClimatePresetModesCardFeature,
122+
"counter-actions": supportsCounterActionsCardFeature,
123+
"cover-open-close": supportsCoverOpenCloseCardFeature,
124+
"cover-position-favorite": supportsCoverPositionFavoriteCardFeature,
125+
"cover-position": supportsCoverPositionCardFeature,
126+
"cover-tilt-favorite": supportsCoverTiltFavoriteCardFeature,
127+
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
128+
"cover-tilt": supportsCoverTiltCardFeature,
129+
"date-set": supportsDateSetCardFeature,
130+
"fan-direction": supportsFanDirectionCardFeature,
131+
"fan-oscillate": supportsFanOscilatteCardFeature,
132+
"fan-preset-modes": supportsFanPresetModesCardFeature,
133+
"fan-speed": supportsFanSpeedCardFeature,
134+
"humidifier-modes": supportsHumidifierModesCardFeature,
135+
"humidifier-toggle": supportsHumidifierToggleCardFeature,
136+
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
137+
"light-brightness": supportsLightBrightnessCardFeature,
138+
"light-color-temp": supportsLightColorTempCardFeature,
139+
"light-color-favorites": supportsLightColorFavoritesCardFeature,
140+
"lock-commands": supportsLockCommandsCardFeature,
141+
"lock-open-door": supportsLockOpenDoorCardFeature,
142+
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
143+
"media-player-sound-mode": supportsMediaPlayerSoundModeCardFeature,
144+
"media-player-source": supportsMediaPlayerSourceCardFeature,
145+
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
146+
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
147+
"numeric-input": supportsNumericInputCardFeature,
148+
"select-options": supportsSelectOptionsCardFeature,
149+
"trend-graph": supportsTrendGraphCardFeature,
150+
"target-humidity": supportsTargetHumidityCardFeature,
151+
"target-temperature": supportsTargetTemperatureCardFeature,
152+
toggle: supportsToggleCardFeature,
153+
"update-actions": supportsUpdateActionsCardFeature,
154+
"vacuum-commands": supportsVacuumCommandsCardFeature,
155+
"valve-open-close": supportsValveOpenCloseCardFeature,
156+
"valve-position-favorite": supportsValvePositionFavoriteCardFeature,
157+
"valve-position": supportsValvePositionCardFeature,
158+
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
159+
};
160+
161+
export const supportsFeatureType = (
162+
hass: HomeAssistant,
163+
context: LovelaceCardFeatureContext,
164+
type: UiFeatureType
165+
): boolean => SUPPORTS_FEATURE_TYPES[type](hass, context);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { computeDomain } from "../../../common/entity/compute_domain";
2+
import type { CalendarCardConfig } from "../cards/types";
3+
import type { CardSuggestionProvider } from "./types";
4+
5+
export const calendarCardSuggestions: CardSuggestionProvider<CalendarCardConfig> =
6+
{
7+
getEntitySuggestion(hass, entityId) {
8+
if (computeDomain(entityId) !== "calendar") return null;
9+
return {
10+
id: "calendar",
11+
label: hass.localize(
12+
"ui.panel.lovelace.editor.cardpicker.suggestions.calendar"
13+
),
14+
config: { type: "calendar", entities: [entityId] },
15+
};
16+
},
17+
};
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { computeDomain } from "../../../common/entity/compute_domain";
2+
import type { HomeAssistant } from "../../../types";
3+
import {
4+
SUPPORTS_FEATURE_TYPES,
5+
type UiFeatureType,
6+
} from "../card-features/registry";
7+
import type { LovelaceCardFeatureConfig } from "../card-features/types";
8+
import type { TileCardConfig } from "../cards/types";
9+
import type { CardSuggestion, CardSuggestionProvider } from "./types";
10+
11+
interface TileVariant {
12+
id: string;
13+
features: UiFeatureType[];
14+
}
15+
16+
const LABEL_PREFIX = "ui.panel.lovelace.editor.cardpicker.suggestions.";
17+
18+
const TILE_VARIANT: TileVariant = { id: "tile", features: [] };
19+
const TILE_TOGGLE_VARIANT: TileVariant = {
20+
id: "tile_toggle",
21+
features: ["toggle"],
22+
};
23+
24+
const SELECT_VARIANTS: TileVariant[] = [
25+
TILE_VARIANT,
26+
{ id: "tile_options", features: ["select-options"] },
27+
];
28+
29+
const NUMERIC_INPUT_VARIANTS: TileVariant[] = [
30+
TILE_VARIANT,
31+
{ id: "tile_numeric_input", features: ["numeric-input"] },
32+
];
33+
34+
const DATE_VARIANTS: TileVariant[] = [
35+
TILE_VARIANT,
36+
{ id: "tile_date_picker", features: ["date-set"] },
37+
];
38+
39+
const DOMAIN_VARIANTS: Record<string, TileVariant[]> = {
40+
light: [
41+
TILE_VARIANT,
42+
{ id: "tile_brightness", features: ["light-brightness"] },
43+
TILE_TOGGLE_VARIANT,
44+
{ id: "tile_color_temperature", features: ["light-color-temp"] },
45+
{ id: "tile_favorite_colors", features: ["light-color-favorites"] },
46+
],
47+
cover: [
48+
TILE_VARIANT,
49+
{ id: "tile_open_close", features: ["cover-open-close"] },
50+
{ id: "tile_position", features: ["cover-position"] },
51+
{ id: "tile_tilt", features: ["cover-tilt"] },
52+
],
53+
climate: [
54+
TILE_VARIANT,
55+
{ id: "tile_hvac_modes", features: ["climate-hvac-modes"] },
56+
],
57+
media_player: [
58+
TILE_VARIANT,
59+
{ id: "tile_playback_controls", features: ["media-player-playback"] },
60+
{ id: "tile_volume_slider", features: ["media-player-volume-slider"] },
61+
],
62+
fan: [
63+
TILE_VARIANT,
64+
{ id: "tile_speed", features: ["fan-speed"] },
65+
{ id: "tile_preset_modes", features: ["fan-preset-modes"] },
66+
],
67+
switch: [TILE_VARIANT, TILE_TOGGLE_VARIANT],
68+
input_boolean: [TILE_VARIANT, TILE_TOGGLE_VARIANT],
69+
lock: [
70+
TILE_VARIANT,
71+
{ id: "tile_lock_commands", features: ["lock-commands"] },
72+
],
73+
humidifier: [
74+
TILE_VARIANT,
75+
{ id: "tile_humidifier_toggle", features: ["humidifier-toggle"] },
76+
{ id: "tile_humidifier_modes", features: ["humidifier-modes"] },
77+
],
78+
vacuum: [
79+
TILE_VARIANT,
80+
{ id: "tile_vacuum_commands", features: ["vacuum-commands"] },
81+
],
82+
lawn_mower: [
83+
TILE_VARIANT,
84+
{ id: "tile_mower_commands", features: ["lawn-mower-commands"] },
85+
],
86+
valve: [
87+
TILE_VARIANT,
88+
{ id: "tile_open_close", features: ["valve-open-close"] },
89+
{ id: "tile_position", features: ["valve-position"] },
90+
],
91+
alarm_control_panel: [
92+
TILE_VARIANT,
93+
{ id: "tile_alarm_modes", features: ["alarm-modes"] },
94+
],
95+
counter: [
96+
TILE_VARIANT,
97+
{ id: "tile_counter_actions", features: ["counter-actions"] },
98+
],
99+
input_select: SELECT_VARIANTS,
100+
select: SELECT_VARIANTS,
101+
input_number: NUMERIC_INPUT_VARIANTS,
102+
number: NUMERIC_INPUT_VARIANTS,
103+
input_datetime: DATE_VARIANTS,
104+
date: DATE_VARIANTS,
105+
update: [
106+
TILE_VARIANT,
107+
{ id: "tile_update_actions", features: ["update-actions"] },
108+
],
109+
water_heater: [
110+
TILE_VARIANT,
111+
{ id: "tile_operation_modes", features: ["water-heater-operation-modes"] },
112+
],
113+
};
114+
115+
const DEFAULT_VARIANT: TileVariant = TILE_VARIANT;
116+
117+
const SENSOR_TREND_DEVICE_CLASSES = new Set<string>([
118+
"battery",
119+
"carbon_dioxide",
120+
"carbon_monoxide",
121+
"humidity",
122+
"illuminance",
123+
"pm1",
124+
"pm10",
125+
"pm25",
126+
"power",
127+
"pressure",
128+
"temperature",
129+
"volatile_organic_compounds",
130+
"wind_speed",
131+
]);
132+
133+
const SENSOR_TREND_VARIANTS: TileVariant[] = [
134+
TILE_VARIANT,
135+
{ id: "tile_trend_graph", features: ["trend-graph"] },
136+
];
137+
138+
// Domains with a dedicated card-suggestions provider; skip the tile
139+
// fallback so the dedicated card wins.
140+
const EXCLUDED_DOMAINS = new Set(["calendar", "todo"]);
141+
142+
const getVariants = (
143+
states: HomeAssistant["states"],
144+
entityId: string
145+
): TileVariant[] | undefined => {
146+
const domain = computeDomain(entityId);
147+
if (domain === "sensor") {
148+
const deviceClass = states[entityId]?.attributes.device_class;
149+
if (deviceClass && SENSOR_TREND_DEVICE_CLASSES.has(deviceClass)) {
150+
return SENSOR_TREND_VARIANTS;
151+
}
152+
return undefined;
153+
}
154+
return DOMAIN_VARIANTS[domain];
155+
};
156+
157+
const buildTileConfig = (
158+
entityId: string,
159+
features: UiFeatureType[]
160+
): TileCardConfig => {
161+
const config: TileCardConfig = { type: "tile", entity: entityId };
162+
if (features.length) {
163+
config.features = features.map(
164+
(type) => ({ type }) as LovelaceCardFeatureConfig
165+
);
166+
}
167+
return config;
168+
};
169+
170+
// A throwing supportsX would invalidate the variant; treat it as unsupported
171+
// rather than tearing down the whole suggestion list.
172+
const allFeaturesSupported = (
173+
hass: HomeAssistant,
174+
entityId: string,
175+
features: UiFeatureType[]
176+
): boolean =>
177+
features.every((type) => {
178+
try {
179+
return SUPPORTS_FEATURE_TYPES[type](hass, { entity_id: entityId });
180+
} catch {
181+
return false;
182+
}
183+
});
184+
185+
export const tileCardSuggestions: CardSuggestionProvider<TileCardConfig> = {
186+
getEntitySuggestion(hass, entityId) {
187+
if (EXCLUDED_DOMAINS.has(computeDomain(entityId))) return null;
188+
const variants = getVariants(hass.states, entityId) ?? [DEFAULT_VARIANT];
189+
const suggestions: CardSuggestion<TileCardConfig>[] = [];
190+
for (const variant of variants) {
191+
if (!allFeaturesSupported(hass, entityId, variant.features)) continue;
192+
suggestions.push({
193+
id: variant.id,
194+
label: hass.localize(`${LABEL_PREFIX}${variant.id}`),
195+
config: buildTileConfig(entityId, variant.features),
196+
});
197+
}
198+
return suggestions.length ? suggestions : null;
199+
},
200+
};

0 commit comments

Comments
 (0)