Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions src/panels/lovelace/card-features/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import type { HomeAssistant } from "../../../types";
import { supportsAlarmModesCardFeature } from "./hui-alarm-modes-card-feature";
import { supportsAreaControlsCardFeature } from "./hui-area-controls-card-feature";
import { supportsBarGaugeCardFeature } from "./hui-bar-gauge-card-feature";
import { supportsButtonCardFeature } from "./hui-button-card-feature";
import { supportsClimateFanModesCardFeature } from "./hui-climate-fan-modes-card-feature";
import { supportsClimateHvacModesCardFeature } from "./hui-climate-hvac-modes-card-feature";
import { supportsClimatePresetModesCardFeature } from "./hui-climate-preset-modes-card-feature";
import { supportsClimateSwingHorizontalModesCardFeature } from "./hui-climate-swing-horizontal-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "./hui-climate-swing-modes-card-feature";
import { supportsCounterActionsCardFeature } from "./hui-counter-actions-card-feature";
import { supportsCoverOpenCloseCardFeature } from "./hui-cover-open-close-card-feature";
import { supportsCoverPositionFavoriteCardFeature } from "./hui-cover-position-favorite-card-feature";
import { supportsCoverPositionCardFeature } from "./hui-cover-position-card-feature";
import { supportsCoverTiltCardFeature } from "./hui-cover-tilt-card-feature";
import { supportsCoverTiltFavoriteCardFeature } from "./hui-cover-tilt-favorite-card-feature";
import { supportsCoverTiltPositionCardFeature } from "./hui-cover-tilt-position-card-feature";
import { supportsDateSetCardFeature } from "./hui-date-set-card-feature";
import { supportsFanDirectionCardFeature } from "./hui-fan-direction-card-feature";
import { supportsFanOscilatteCardFeature } from "./hui-fan-oscillate-card-feature";
import { supportsFanPresetModesCardFeature } from "./hui-fan-preset-modes-card-feature";
import { supportsFanSpeedCardFeature } from "./hui-fan-speed-card-feature";
import { supportsHumidifierModesCardFeature } from "./hui-humidifier-modes-card-feature";
import { supportsHumidifierToggleCardFeature } from "./hui-humidifier-toggle-card-feature";
import { supportsLawnMowerCommandCardFeature } from "./hui-lawn-mower-commands-card-feature";
import { supportsLightBrightnessCardFeature } from "./hui-light-brightness-card-feature";
import { supportsLightColorFavoritesCardFeature } from "./hui-light-color-favorites-card-feature";
import { supportsLightColorTempCardFeature } from "./hui-light-color-temp-card-feature";
import { supportsLockCommandsCardFeature } from "./hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "./hui-lock-open-door-card-feature";
import { supportsMediaPlayerPlaybackCardFeature } from "./hui-media-player-playback-card-feature";
import { supportsMediaPlayerSoundModeCardFeature } from "./hui-media-player-sound-mode-card-feature";
import { supportsMediaPlayerSourceCardFeature } from "./hui-media-player-source-card-feature";
import { supportsMediaPlayerVolumeButtonsCardFeature } from "./hui-media-player-volume-buttons-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "./hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "./hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "./hui-select-options-card-feature";
import { supportsTargetHumidityCardFeature } from "./hui-target-humidity-card-feature";
import { supportsTargetTemperatureCardFeature } from "./hui-target-temperature-card-feature";
import { supportsToggleCardFeature } from "./hui-toggle-card-feature";
import { supportsTrendGraphCardFeature } from "./hui-trend-graph-card-feature";
import { supportsUpdateActionsCardFeature } from "./hui-update-actions-card-feature";
import { supportsVacuumCommandsCardFeature } from "./hui-vacuum-commands-card-feature";
import { supportsValveOpenCloseCardFeature } from "./hui-valve-open-close-card-feature";
import { supportsValvePositionFavoriteCardFeature } from "./hui-valve-position-favorite-card-feature";
import { supportsValvePositionCardFeature } from "./hui-valve-position-card-feature";
import { supportsWaterHeaterOperationModesCardFeature } from "./hui-water-heater-operation-modes-card-feature";
import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";

export type FeatureType = LovelaceCardFeatureConfig["type"];

export type SupportsFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => boolean;

export const UI_FEATURE_TYPES = [
"alarm-modes",
"area-controls",
"bar-gauge",
"button",
"climate-fan-modes",
"climate-hvac-modes",
"climate-preset-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
"counter-actions",
"cover-open-close",
"cover-position-favorite",
"cover-position",
"cover-tilt-favorite",
"cover-tilt-position",
"cover-tilt",
"date-set",
"fan-direction",
"fan-oscillate",
"fan-preset-modes",
"fan-speed",
"humidifier-modes",
"humidifier-toggle",
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"light-color-favorites",
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-sound-mode",
"media-player-source",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
"trend-graph",
"target-humidity",
"target-temperature",
"toggle",
"update-actions",
"vacuum-commands",
"valve-open-close",
"valve-position-favorite",
"valve-position",
"water-heater-operation-modes",
] as const satisfies readonly FeatureType[];

export type UiFeatureType = (typeof UI_FEATURE_TYPES)[number];

export const SUPPORTS_FEATURE_TYPES: Record<UiFeatureType, SupportsFeature> = {
"alarm-modes": supportsAlarmModesCardFeature,
"area-controls": supportsAreaControlsCardFeature,
"bar-gauge": supportsBarGaugeCardFeature,
button: supportsButtonCardFeature,
"climate-fan-modes": supportsClimateFanModesCardFeature,
"climate-swing-modes": supportsClimateSwingModesCardFeature,
"climate-swing-horizontal-modes":
supportsClimateSwingHorizontalModesCardFeature,
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
"climate-preset-modes": supportsClimatePresetModesCardFeature,
"counter-actions": supportsCounterActionsCardFeature,
"cover-open-close": supportsCoverOpenCloseCardFeature,
"cover-position-favorite": supportsCoverPositionFavoriteCardFeature,
"cover-position": supportsCoverPositionCardFeature,
"cover-tilt-favorite": supportsCoverTiltFavoriteCardFeature,
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
"cover-tilt": supportsCoverTiltCardFeature,
"date-set": supportsDateSetCardFeature,
"fan-direction": supportsFanDirectionCardFeature,
"fan-oscillate": supportsFanOscilatteCardFeature,
"fan-preset-modes": supportsFanPresetModesCardFeature,
"fan-speed": supportsFanSpeedCardFeature,
"humidifier-modes": supportsHumidifierModesCardFeature,
"humidifier-toggle": supportsHumidifierToggleCardFeature,
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
"light-brightness": supportsLightBrightnessCardFeature,
"light-color-temp": supportsLightColorTempCardFeature,
"light-color-favorites": supportsLightColorFavoritesCardFeature,
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-sound-mode": supportsMediaPlayerSoundModeCardFeature,
"media-player-source": supportsMediaPlayerSourceCardFeature,
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,
"trend-graph": supportsTrendGraphCardFeature,
"target-humidity": supportsTargetHumidityCardFeature,
"target-temperature": supportsTargetTemperatureCardFeature,
toggle: supportsToggleCardFeature,
"update-actions": supportsUpdateActionsCardFeature,
"vacuum-commands": supportsVacuumCommandsCardFeature,
"valve-open-close": supportsValveOpenCloseCardFeature,
"valve-position-favorite": supportsValvePositionFavoriteCardFeature,
"valve-position": supportsValvePositionCardFeature,
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
};

export const supportsFeatureType = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext,
type: UiFeatureType
): boolean => SUPPORTS_FEATURE_TYPES[type](hass, context);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { computeDomain } from "../../../common/entity/compute_domain";
import type { CalendarCardConfig } from "../cards/types";
import type { CardSuggestionProvider } from "./types";

export const calendarCardSuggestions: CardSuggestionProvider<CalendarCardConfig> =
{
getEntitySuggestion(hass, entityId) {
if (computeDomain(entityId) !== "calendar") return null;
return {
id: "calendar",
label: hass.localize(
"ui.panel.lovelace.editor.cardpicker.suggestions.calendar"
),
config: { type: "calendar", entities: [entityId] },
};
},
};
200 changes: 200 additions & 0 deletions src/panels/lovelace/card-suggestions/hui-tile-card-suggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HomeAssistant } from "../../../types";
import {
SUPPORTS_FEATURE_TYPES,
type UiFeatureType,
} from "../card-features/registry";
import type { LovelaceCardFeatureConfig } from "../card-features/types";
import type { TileCardConfig } from "../cards/types";
import type { CardSuggestion, CardSuggestionProvider } from "./types";

interface TileVariant {
id: string;
features: UiFeatureType[];
}

const LABEL_PREFIX = "ui.panel.lovelace.editor.cardpicker.suggestions.";

const TILE_VARIANT: TileVariant = { id: "tile", features: [] };
const TILE_TOGGLE_VARIANT: TileVariant = {
id: "tile_toggle",
features: ["toggle"],
};

const SELECT_VARIANTS: TileVariant[] = [
TILE_VARIANT,
{ id: "tile_options", features: ["select-options"] },
];

const NUMERIC_INPUT_VARIANTS: TileVariant[] = [
TILE_VARIANT,
{ id: "tile_numeric_input", features: ["numeric-input"] },
];

const DATE_VARIANTS: TileVariant[] = [
TILE_VARIANT,
{ id: "tile_date_picker", features: ["date-set"] },
];

const DOMAIN_VARIANTS: Record<string, TileVariant[]> = {
light: [
TILE_VARIANT,
{ id: "tile_brightness", features: ["light-brightness"] },
TILE_TOGGLE_VARIANT,
{ id: "tile_color_temperature", features: ["light-color-temp"] },
{ id: "tile_favorite_colors", features: ["light-color-favorites"] },
],
cover: [
TILE_VARIANT,
{ id: "tile_open_close", features: ["cover-open-close"] },
{ id: "tile_position", features: ["cover-position"] },
{ id: "tile_tilt", features: ["cover-tilt"] },
],
climate: [
TILE_VARIANT,
{ id: "tile_hvac_modes", features: ["climate-hvac-modes"] },
],
media_player: [
TILE_VARIANT,
{ id: "tile_playback_controls", features: ["media-player-playback"] },
{ id: "tile_volume_slider", features: ["media-player-volume-slider"] },
],
fan: [
TILE_VARIANT,
{ id: "tile_speed", features: ["fan-speed"] },
{ id: "tile_preset_modes", features: ["fan-preset-modes"] },
],
switch: [TILE_VARIANT, TILE_TOGGLE_VARIANT],
input_boolean: [TILE_VARIANT, TILE_TOGGLE_VARIANT],
lock: [
TILE_VARIANT,
{ id: "tile_lock_commands", features: ["lock-commands"] },
],
humidifier: [
TILE_VARIANT,
{ id: "tile_humidifier_toggle", features: ["humidifier-toggle"] },
{ id: "tile_humidifier_modes", features: ["humidifier-modes"] },
],
vacuum: [
TILE_VARIANT,
{ id: "tile_vacuum_commands", features: ["vacuum-commands"] },
],
lawn_mower: [
TILE_VARIANT,
{ id: "tile_mower_commands", features: ["lawn-mower-commands"] },
],
valve: [
TILE_VARIANT,
{ id: "tile_open_close", features: ["valve-open-close"] },
{ id: "tile_position", features: ["valve-position"] },
],
alarm_control_panel: [
TILE_VARIANT,
{ id: "tile_alarm_modes", features: ["alarm-modes"] },
],
counter: [
TILE_VARIANT,
{ id: "tile_counter_actions", features: ["counter-actions"] },
],
input_select: SELECT_VARIANTS,
select: SELECT_VARIANTS,
input_number: NUMERIC_INPUT_VARIANTS,
number: NUMERIC_INPUT_VARIANTS,
input_datetime: DATE_VARIANTS,
date: DATE_VARIANTS,
update: [
TILE_VARIANT,
{ id: "tile_update_actions", features: ["update-actions"] },
],
water_heater: [
TILE_VARIANT,
{ id: "tile_operation_modes", features: ["water-heater-operation-modes"] },
],
};

const DEFAULT_VARIANT: TileVariant = TILE_VARIANT;

const SENSOR_TREND_DEVICE_CLASSES = new Set<string>([
"battery",
"carbon_dioxide",
"carbon_monoxide",
"humidity",
"illuminance",
"pm1",
"pm10",
"pm25",
"power",
"pressure",
"temperature",
"volatile_organic_compounds",
"wind_speed",
]);

const SENSOR_TREND_VARIANTS: TileVariant[] = [
TILE_VARIANT,
{ id: "tile_trend_graph", features: ["trend-graph"] },
];

// Domains with a dedicated card-suggestions provider; skip the tile
// fallback so the dedicated card wins.
const EXCLUDED_DOMAINS = new Set(["calendar", "todo"]);

const getVariants = (
states: HomeAssistant["states"],
entityId: string
): TileVariant[] | undefined => {
const domain = computeDomain(entityId);
if (domain === "sensor") {
const deviceClass = states[entityId]?.attributes.device_class;
if (deviceClass && SENSOR_TREND_DEVICE_CLASSES.has(deviceClass)) {
return SENSOR_TREND_VARIANTS;
}
return undefined;
}
return DOMAIN_VARIANTS[domain];
};

const buildTileConfig = (
entityId: string,
features: UiFeatureType[]
): TileCardConfig => {
const config: TileCardConfig = { type: "tile", entity: entityId };
if (features.length) {
config.features = features.map(
(type) => ({ type }) as LovelaceCardFeatureConfig
);
}
return config;
};

// A throwing supportsX would invalidate the variant; treat it as unsupported
// rather than tearing down the whole suggestion list.
const allFeaturesSupported = (
hass: HomeAssistant,
entityId: string,
features: UiFeatureType[]
): boolean =>
features.every((type) => {
try {
return SUPPORTS_FEATURE_TYPES[type](hass, { entity_id: entityId });
} catch {
return false;
}
});

export const tileCardSuggestions: CardSuggestionProvider<TileCardConfig> = {
getEntitySuggestion(hass, entityId) {
if (EXCLUDED_DOMAINS.has(computeDomain(entityId))) return null;
const variants = getVariants(hass.states, entityId) ?? [DEFAULT_VARIANT];
const suggestions: CardSuggestion<TileCardConfig>[] = [];
for (const variant of variants) {
if (!allFeaturesSupported(hass, entityId, variant.features)) continue;
suggestions.push({
id: variant.id,
label: hass.localize(`${LABEL_PREFIX}${variant.id}`),
config: buildTileConfig(entityId, variant.features),
});
}
return suggestions.length ? suggestions : null;
},
};
Loading
Loading