diff --git a/src/panels/lovelace/cards/hui-entities-card.js b/src/panels/lovelace/cards/hui-entities-card.js
deleted file mode 100644
index 85e89af5f3..0000000000
--- a/src/panels/lovelace/cards/hui-entities-card.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import "@polymer/iron-flex-layout/iron-flex-layout-classes.js";
-import { html } from "@polymer/polymer/lib/utils/html-tag.js";
-import { PolymerElement } from "@polymer/polymer/polymer-element.js";
-
-import "../../../components/ha-card.js";
-import "../components/hui-entities-toggle.js";
-
-import createRowElement from "../common/create-row-element.js";
-import computeDomain from "../../../common/entity/compute_domain.js";
-import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const.js";
-
-import EventsMixin from "../../../mixins/events-mixin.js";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HuiEntitiesCard extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
-
-
-
-
-`;
- }
-
- static get properties() {
- return {
- hass: {
- type: Object,
- observer: "_hassChanged",
- },
- _config: Object,
- };
- }
-
- constructor() {
- super();
- this._elements = [];
- }
-
- ready() {
- super.ready();
- if (this._config) this._buildConfig();
- }
-
- getCardSize() {
- // +1 for the header
- return 1 + (this._config ? this._config.entities.length : 0);
- }
-
- _showHeaderToggle(show) {
- // If show is undefined, we treat it as true
- return show !== false;
- }
-
- _showHeader(config) {
- // Show header if either title or toggle configured to show in it
- return config.title || config.show_header_toggle;
- }
-
- setConfig(config) {
- this._config = config;
- this._rows = config.entities.map(
- (item) => (typeof item === "string" ? { entity: item } : item)
- );
- if (this.$) this._buildConfig();
- }
-
- _buildConfig() {
- const root = this.$.states;
-
- while (root.lastChild) {
- root.removeChild(root.lastChild);
- }
-
- this._elements = [];
-
- for (const row of this._rows) {
- const entityId = row.entity;
- const element = createRowElement(row);
- if (
- entityId &&
- !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityId))
- ) {
- element.classList.add("state-card-dialog");
- element.addEventListener("click", () =>
- this.fire("hass-more-info", { entityId })
- );
- }
- element.hass = this.hass;
- this._elements.push(element);
- const container = document.createElement("div");
- container.appendChild(element);
- root.appendChild(container);
- }
- }
-
- _hassChanged(hass) {
- this._elements.forEach((element) => {
- element.hass = hass;
- });
- }
-
- _filterEntities(items) {
- return items
- .filter((item) => typeof item === "string" || item.entity)
- .map((item) => (typeof item === "string" ? item : item.entity));
- }
-}
-
-customElements.define("hui-entities-card", HuiEntitiesCard);
diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts
new file mode 100644
index 0000000000..d0af91aebf
--- /dev/null
+++ b/src/panels/lovelace/cards/hui-entities-card.ts
@@ -0,0 +1,194 @@
+import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
+import { repeat } from "lit-html/directives/repeat";
+
+import "../../../components/ha-card.js";
+import "../components/hui-entities-toggle.js";
+
+import { fireEvent } from "../../../common/dom/fire_event.js";
+import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const.js";
+import { HassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
+import { LovelaceCard, LovelaceConfig } from "../types.js";
+import { HomeAssistant } from "../../../types.js";
+import createRowElement from "../common/create-row-element.js";
+import computeDomain from "../../../common/entity/compute_domain.js";
+import processConfigEntities from "../common/process-config-entities";
+
+interface EntityConfig {
+ name?: string;
+ icon?: string;
+ entity: string;
+ type?: string;
+ secondary_info: "entity-id" | "last-changed";
+ action_name?: string;
+ service?: string;
+ service_data?: object;
+ url?: string;
+}
+
+interface Config extends LovelaceConfig {
+ show_header_toggle?: boolean;
+ title?: string;
+ entities: EntityConfig[];
+}
+
+class HuiEntitiesCard extends HassLocalizeLitMixin(LitElement)
+ implements LovelaceCard {
+ protected hass?: HomeAssistant;
+ protected config?: Config;
+ protected configEntities?: EntityConfig[];
+
+ static get properties(): PropertyDeclarations {
+ return {
+ hass: {},
+ config: {},
+ };
+ }
+
+ public getCardSize() {
+ // +1 for the header
+ return 1 + (this.config ? this.config.entities.length : 0);
+ }
+
+ public setConfig(config: Config) {
+ this.config = { show_header_toggle: true, ...config };
+ const entities = processConfigEntities(config.entities);
+
+ for (const entity of entities) {
+ if (
+ entity.type === "call-service" &&
+ !entity.service &&
+ !entity.name &&
+ !entity.icon &&
+ !entity.service_data &&
+ !entity.action_name
+ ) {
+ throw new Error("Missing required property when type is call-service");
+ } else if (
+ entity.type === "weblink" &&
+ !entity.name &&
+ !entity.icon &&
+ !entity.url
+ ) {
+ throw new Error("Missing required property when type is weblink");
+ }
+ }
+
+ this.configEntities = entities;
+ if (this.hass) {
+ this.requestUpdate();
+ }
+ }
+
+ protected render() {
+ if (!this.config || !this.hass) {
+ return html``;
+ }
+ const { show_header_toggle, title } = this.config;
+ const states = this.hass.states;
+
+ const stateEntities = this.configEntities!.filter(
+ (conf) => conf.entity in states
+ );
+
+ const rowEntities = this.configEntities!.filter(
+ (item) => typeof item === "string" || item.entity
+ ).map((item) => (typeof item === "string" ? item : item.entity));
+
+ return html`
+ ${this.renderStyle()}
+
+ ${
+ !title && !show_header_toggle
+ ? html``
+ : html`
+ `
+ }
+
+ ${repeat(
+ stateEntities,
+ (entityConf) => entityConf.entity,
+ (entityConf) => this.renderEntity(entityConf)
+ )}
+
+
+ `;
+ }
+
+ private renderStyle() {
+ return html`
+
+ `;
+ }
+
+ private renderEntity(entityConf) {
+ const element = createRowElement(entityConf);
+ element.hass = this.hass;
+ element.entityConf = entityConf;
+ if (!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity))) {
+ element.classList.add("state-card-dialog");
+ element.onclick = this.handleClick;
+ }
+
+ return html`
+
+ ${element}
+
+ `;
+ }
+
+ private handleClick(ev: MouseEvent) {
+ const config = (ev.currentTarget as any).entityConf as EntityConfig;
+ const entityId = config.entity;
+
+ fireEvent(this, "hass-more-info", { entityId });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-entities-card": HuiEntitiesCard;
+ }
+}
+
+customElements.define("hui-entities-card", HuiEntitiesCard);
diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js
index 73ee2bf8af..c7d1a876ee 100644
--- a/src/panels/lovelace/common/create-card-element.js
+++ b/src/panels/lovelace/common/create-card-element.js
@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event.js";
import "../cards/hui-alarm-panel-card.js";
import "../cards/hui-conditional-card.ts";
-import "../cards/hui-entities-card.js";
+import "../cards/hui-entities-card.ts";
import "../cards/hui-entity-button-card.ts";
import "../cards/hui-entity-filter-card.js";
import "../cards/hui-error-card.js";
diff --git a/src/panels/lovelace/hui-unused-entities.js b/src/panels/lovelace/hui-unused-entities.js
index 196d464b48..9b9e318954 100644
--- a/src/panels/lovelace/hui-unused-entities.js
+++ b/src/panels/lovelace/hui-unused-entities.js
@@ -4,7 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
import computeUnusedEntities from "./common/compute-unused-entities.js";
import createCardElement from "./common/create-card-element.js";
-import "./cards/hui-entities-card.js";
+import "./cards/hui-entities-card.ts";
class HuiUnusedEntities extends PolymerElement {
static get template() {