Add support for custom tile features (#15411

* Move isSupported to class

* Use getConfigElement to display edit button

* Remove custom element for test

* Don't use class static functions

* Refactor render

* Add support for custom tile features in editor

* Add missing relative position

* Rename custom tile features options

* PR feedbacks + split offical and custom types

* Merge is custom type function
This commit is contained in:
Paul Bottein 2023-02-21 11:10:44 +01:00 committed by GitHub
parent 88205a94d6
commit 8ed4914232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 275 additions and 164 deletions

View File

@ -1,6 +1,5 @@
import { refine, string } from "superstruct";
export const isCustomType = (value: string) => value.startsWith("custom:");
import { isCustomType } from "../../data/lovelace_custom_cards";
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@ -1,3 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
export interface CustomCardEntry {
type: string;
name?: string;
@ -6,8 +8,16 @@ export interface CustomCardEntry {
documentationURL?: string;
}
export interface CustomTileFeatureEntry {
type: string;
name?: string;
supported?: (stateObj: HassEntity) => boolean;
configurable?: boolean;
}
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
customTileFeatures?: CustomTileFeatureEntry[];
}
export const CUSTOM_TYPE_PREFIX = "custom:";
@ -17,8 +27,18 @@ const customCardsWindow = window as CustomCardsWindow;
if (!("customCards" in customCardsWindow)) {
customCardsWindow.customCards = [];
}
if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = [];
}
export const customCards = customCardsWindow.customCards!;
export const customTileFeatures = customCardsWindow.customTileFeatures!;
export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);
export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX);
export const stripCustomPrefix = (type: string) =>
type.slice(CUSTOM_TYPE_PREFIX.length);

View File

@ -40,7 +40,6 @@ import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import "../components/hui-timestamp-display";
import { createTileFeatureElement } from "../create-element/create-tile-feature-element";
import { supportsTileFeature } from "../tile-features/tile-features";
import { LovelaceTileFeatureConfig } from "../tile-features/types";
import {
LovelaceCard,
@ -309,10 +308,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
: undefined;
const badge = computeTileBadge(stateObj, this.hass);
const supportedFeatures = this._config.features?.filter((feature) =>
supportsTileFeature(stateObj, feature.type)
);
return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : null}
@ -373,15 +368,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
></ha-tile-info>
</div>
</div>
${supportedFeatures?.length
? html`
<div class="features">
${supportedFeatures.map((featureConf) =>
this.renderFeature(featureConf, stateObj)
)}
</div>
`
: null}
<div class="features">
${this._config.features?.map((featureConf) =>
this.renderFeature(featureConf, stateObj)
)}
</div>
</ha-card>
`;
}
@ -412,7 +403,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
(element as LovelaceTileFeature).stateObj = stateObj;
}
return html`<div class="feature">${element}</div>`;
return html`${element}`;
}
static get styles(): CSSResultGroup {
@ -502,6 +493,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
box-sizing: border-box;
pointer-events: none;
}
.features {
position: relative;
}
`;
}
}

View File

@ -5,7 +5,10 @@ import {
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import { CUSTOM_TYPE_PREFIX } from "../../../data/lovelace_custom_cards";
import {
isCustomType,
stripCustomPrefix,
} from "../../../data/lovelace_custom_cards";
import type { HuiErrorCard } from "../cards/hui-error-card";
import type { ErrorCardConfig } from "../cards/types";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
@ -153,9 +156,7 @@ const _lazyCreate = <T extends keyof CreateElementConfigTypes>(
};
const _getCustomTag = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX)
? type.slice(CUSTOM_TYPE_PREFIX.length)
: undefined;
isCustomType(type) ? stripCustomPrefix(type) : undefined;
export const createLovelaceElement = <T extends keyof CreateElementConfigTypes>(
tagSuffix: T,

View File

@ -17,10 +17,7 @@ import {
union,
} from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import {
customType,
isCustomType,
} from "../../../../common/structs/is-custom-type";
import { customType } from "../../../../common/structs/is-custom-type";
import { entityId } from "../../../../common/structs/is-entity-id";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/entity/state-badge";
@ -50,6 +47,7 @@ import {
} from "../types";
import { configElementStyle } from "./config-elements-style";
import { buttonEntityConfigStruct } from "../structs/button-entity-struct";
import { isCustomType } from "../../../../data/lovelace_custom_cards";
const buttonEntitiesRowConfigStruct = object({
type: literal("button"),

View File

@ -1,11 +1,4 @@
import {
mdiDelete,
mdiDrag,
mdiListBox,
mdiPencil,
mdiPlus,
mdiWindowShutter,
} from "@mdi/js";
import { mdiDelete, mdiDrag, mdiListBox, mdiPencil, mdiPlus } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
@ -14,9 +7,17 @@ import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-svg-icon";
import {
CustomTileFeatureEntry,
customTileFeatures,
CUSTOM_TYPE_PREFIX,
isCustomType,
stripCustomPrefix,
} from "../../../../data/lovelace_custom_cards";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import {
loadSortable,
@ -24,19 +25,40 @@ import {
} from "../../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../../types";
import { getTileFeatureElementClass } from "../../create-element/create-tile-feature-element";
import {
isTileFeatureEditable,
supportsTileFeature,
} from "../../tile-features/tile-features";
import { supportsCoverOpenCloseTileFeature } from "../../tile-features/hui-cover-open-close-tile-feature";
import { supportsCoverTiltTileFeature } from "../../tile-features/hui-cover-tilt-tile-feature";
import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature";
import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
import { LovelaceTileFeatureConfig } from "../../tile-features/types";
const FEATURES_TYPE: LovelaceTileFeatureConfig["type"][] = [
type FeatureType = LovelaceTileFeatureConfig["type"];
type SupportsFeature = (stateObj: HassEntity) => boolean;
const FEATURE_TYPES: FeatureType[] = [
"cover-open-close",
"cover-tilt",
"light-brightness",
"vacuum-commands",
];
const EDITABLES_FEATURE_TYPES = new Set<FeatureType>(["vacuum-commands"]);
const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
{
"cover-open-close": supportsCoverOpenCloseTileFeature,
"cover-tilt": supportsCoverTiltTileFeature,
"light-brightness": supportsLightBrightnessTileFeature,
"vacuum-commands": supportsVacuumCommandTileFeature,
};
const CUSTOM_FEATURE_ENTRIES: Record<
string,
CustomTileFeatureEntry | undefined
> = {};
customTileFeatures.forEach((feature) => {
CUSTOM_FEATURE_ENTRIES[feature.type] = feature;
});
declare global {
interface HASSDomEvents {
"features-changed": {
@ -64,6 +86,45 @@ export class HuiTileCardFeaturesEditor extends LitElement {
this._destroySortable();
}
private _supportsFeatureType(type: string): boolean {
if (!this.stateObj) return false;
if (isCustomType(type)) {
const customType = stripCustomPrefix(type);
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
if (!customFeatureEntry?.supported) return true;
try {
return customFeatureEntry.supported(this.stateObj);
} catch {
return false;
}
}
const supportsFeature = SUPPORTS_FEATURE_TYPES[type];
return !supportsFeature || supportsFeature(this.stateObj);
}
private _isFeatureTypeEditable(type: string) {
if (isCustomType(type)) {
const customType = stripCustomPrefix(type);
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
return customFeatureEntry?.configurable;
}
return EDITABLES_FEATURE_TYPES.has(type as FeatureType);
}
private _getFeatureTypeLabel(type: string) {
if (isCustomType(type)) {
const customType = stripCustomPrefix(type);
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
return customFeatureEntry?.name || type;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
);
}
private _getKey(feature: LovelaceTileFeatureConfig) {
if (!this._featuresKeys.has(feature)) {
this._featuresKeys.set(feature, Math.random().toString());
@ -72,19 +133,32 @@ export class HuiTileCardFeaturesEditor extends LitElement {
return this._featuresKeys.get(feature)!;
}
private get _supportedFeatureTypes() {
if (!this.stateObj) return [];
protected firstUpdated() {
this._createSortable();
}
return FEATURES_TYPE.filter((type) =>
supportsTileFeature(this.stateObj!, type)
private _getSupportedFeaturesType() {
const featuresTypes = FEATURE_TYPES as string[];
const customFeaturesTypes = customTileFeatures.map(
(feature) => `${CUSTOM_TYPE_PREFIX}${feature.type}`
);
return featuresTypes
.concat(customFeaturesTypes)
.filter((type) => this._supportsFeatureType(type));
}
protected render(): TemplateResult {
if (!this.features || !this.hass) {
return html``;
return null;
}
const supportedFeaturesType = this._getSupportedFeaturesType();
const types = supportedFeaturesType.filter((type) => !isCustomType(type));
const customTypes = supportedFeaturesType.filter((type) =>
isCustomType(type)
);
return html`
<ha-expansion-panel outlined>
<h3 slot="header">
@ -94,8 +168,7 @@ export class HuiTileCardFeaturesEditor extends LitElement {
)}
</h3>
<div class="content">
${this._supportedFeatureTypes.length === 0 &&
this.features.length === 0
${supportedFeaturesType.length === 0 && this.features.length === 0
? html`
<ha-alert type="info">
${this.hass!.localize(
@ -108,53 +181,58 @@ export class HuiTileCardFeaturesEditor extends LitElement {
${repeat(
this.features,
(featureConf) => this._getKey(featureConf),
(featureConf, index) => html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<div class="feature-content">
<div>
<span>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.types.${featureConf.type}.label`
)}
</span>
${this.stateObj &&
!supportsTileFeature(this.stateObj, featureConf.type)
? html`<span class="secondary">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.tile.features.not_compatible"
)}
</span>`
: null}
(featureConf, index) => {
const type = featureConf.type;
const supported = this._supportsFeatureType(type);
const editable = this._isFeatureTypeEditable(type);
return html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<div class="feature-content">
<div>
<span> ${this._getFeatureTypeLabel(type)} </span>
${this.stateObj && !supported
? html`
<span class="secondary">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.tile.features.not_compatible"
)}
</span>
`
: null}
</div>
</div>
${editable
? html`
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.edit`
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editFeature}
.disabled=${!supported}
></ha-icon-button>
`
: null}
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.remove`
)}
.path=${mdiDelete}
class="remove-icon"
.index=${index}
@click=${this._removeFeature}
></ha-icon-button>
</div>
${isTileFeatureEditable(featureConf.type)
? html`<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.edit`
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editFeature}
></ha-icon-button>`
: null}
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.remove`
)}
.path=${mdiDelete}
class="remove-icon"
.index=${index}
@click=${this._removeFeature}
></ha-icon-button>
</div>
`
`;
}
)}
</div>
${this._supportedFeatureTypes.length > 0
${supportedFeaturesType.length > 0
? html`
<ha-button-menu
fixed
@ -170,16 +248,22 @@ export class HuiTileCardFeaturesEditor extends LitElement {
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._supportedFeatureTypes.map(
(featureType) => html`<mwc-list-item .value=${featureType}>
<ha-svg-icon
slot="graphic"
.path=${mdiWindowShutter}
></ha-svg-icon>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.types.${featureType}.label`
)}
</mwc-list-item>`
${types.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
${types.length > 0 && customTypes.length > 0
? html`<li divider role="separator"></li>`
: null}
${customTypes.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
</ha-button-menu>
`
@ -189,10 +273,6 @@ export class HuiTileCardFeaturesEditor extends LitElement {
`;
}
protected firstUpdated(): void {
this._createSortable();
}
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(
@ -228,7 +308,9 @@ export class HuiTileCardFeaturesEditor extends LitElement {
if (index == null) return;
const value = this._supportedFeatureTypes[index];
const value = this._getSupportedFeaturesType()[index];
if (!value) return;
const elClass = await getTileFeatureElementClass(value);
let newFeature: LovelaceTileFeatureConfig;
@ -340,6 +422,10 @@ export class HuiTileCardFeaturesEditor extends LitElement {
font-size: 12px;
color: var(--secondary-text-color);
}
li[divider] {
border-bottom-color: var(--divider-color);
}
`,
];
}

View File

@ -1,6 +1,7 @@
import {
CUSTOM_TYPE_PREFIX,
getCustomCardEntry,
isCustomType,
stripCustomPrefix,
} from "../../../data/lovelace_custom_cards";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@ -9,9 +10,8 @@ export const getCardDocumentationURL = (
hass: HomeAssistant,
type: string
): string | undefined => {
if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
return getCustomCardEntry(type.slice(CUSTOM_TYPE_PREFIX.length))
?.documentationURL;
if (isCustomType(type)) {
return getCustomCardEntry(stripCustomPrefix(type))?.documentationURL;
}
return `${documentationUrl(hass, "/lovelace/")}${type}`;

View File

@ -2,6 +2,7 @@ import { mdiStop } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import {
computeCloseIcon,
computeOpenIcon,
@ -19,6 +20,15 @@ import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { CoverOpenCloseTileFeatureConfig } from "./types";
export const supportsCoverOpenCloseTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE))
);
};
@customElement("hui-cover-open-close-tile-feature")
class HuiCoverOpenCloseTileFeature
extends LitElement
@ -64,9 +74,14 @@ class HuiCoverOpenCloseTileFeature
});
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.stateObj) {
return html``;
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsCoverOpenCloseTileFeature(this.stateObj)
) {
return null;
}
return html`

View File

@ -2,6 +2,7 @@ import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
@ -15,6 +16,15 @@ import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { CoverTiltTileFeatureConfig } from "./types";
export const supportsCoverTiltTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT))
);
};
@customElement("hui-cover-tilt-tile-feature")
class HuiCoverTiltTileFeature
extends LitElement
@ -60,9 +70,14 @@ class HuiCoverTiltTileFeature
});
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.stateObj) {
return html``;
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsCoverTiltTileFeature
) {
return null;
}
return html`

View File

@ -1,13 +1,20 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import "../../../components/tile/ha-tile-slider";
import { UNAVAILABLE } from "../../../data/entity";
import { lightSupportsBrightness } from "../../../data/light";
import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { LightBrightnessTileFeatureConfig } from "./types";
export const supportsLightBrightnessTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsBrightness(stateObj);
};
@customElement("hui-light-brightness-tile-feature")
class HuiLightBrightnessTileFeature
extends LitElement
@ -32,9 +39,14 @@ class HuiLightBrightnessTileFeature
this._config = config;
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.stateObj) {
return html``;
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsLightBrightnessTileFeature(this.stateObj)
) {
return null;
}
const position =

View File

@ -10,6 +10,7 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
@ -113,6 +114,14 @@ export const VACUUM_COMMANDS_BUTTONS: Record<
}),
};
export const supportsVacuumCommandTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
domain === "vacuum" &&
VACUUM_COMMANDS.some((c) => supportsVacuumCommand(stateObj, c))
);
};
@customElement("hui-vacuum-commands-tile-feature")
class HuiVacuumCommandTileFeature
extends LitElement
@ -160,9 +169,14 @@ class HuiVacuumCommandTileFeature
});
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.stateObj) {
return html``;
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsVacuumCommandTileFeature(this.stateObj)
) {
return null;
}
const stateObj = this.stateObj as VacuumEntity;

View File

@ -1,44 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { CoverEntityFeature } from "../../../data/cover";
import { lightSupportsBrightness } from "../../../data/light";
import { supportsVacuumCommand } from "./hui-vacuum-commands-tile-feature";
import { LovelaceTileFeatureConfig, VACUUM_COMMANDS } from "./types";
type TileFeatureType = LovelaceTileFeatureConfig["type"];
export type SupportsTileFeature = (stateObj: HassEntity) => boolean;
const TILE_FEATURES_SUPPORT: Record<TileFeatureType, SupportsTileFeature> = {
"cover-open-close": (stateObj) =>
computeDomain(stateObj.entity_id) === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE)),
"cover-tilt": (stateObj) =>
computeDomain(stateObj.entity_id) === "cover" &&
(supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) ||
supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT)),
"light-brightness": (stateObj) =>
computeDomain(stateObj.entity_id) === "light" &&
lightSupportsBrightness(stateObj),
"vacuum-commands": (stateObj) =>
computeDomain(stateObj.entity_id) === "vacuum" &&
VACUUM_COMMANDS.some((c) => supportsVacuumCommand(stateObj, c)),
};
const TILE_FEATURE_EDITABLE: Set<TileFeatureType> = new Set([
"vacuum-commands",
]);
export const supportsTileFeature = (
stateObj: HassEntity,
feature: TileFeatureType
): boolean => {
const supportFunction = TILE_FEATURES_SUPPORT[feature] as
| SupportsTileFeature
| undefined;
return !supportFunction || supportFunction(stateObj);
};
export const isTileFeatureEditable = (feature: TileFeatureType): boolean =>
TILE_FEATURE_EDITABLE.has(feature);

View File

@ -109,6 +109,7 @@ export interface LovelaceTileFeatureConstructor
hass: HomeAssistant,
stateObj?: HassEntity
) => LovelaceTileFeatureConfig;
isSupported?: (stateObj?: HassEntity) => boolean;
}
export interface LovelaceTileFeatureEditor