diff --git a/src/common/const.ts b/src/common/const.ts
index 6ec749d662..40374552e0 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -42,6 +42,7 @@ export const FIXED_DOMAIN_ICONS = {
remote: "hass:remote",
scene: "hass:palette",
script: "hass:script-text",
+ select: "hass:format-list-bulleted",
sensor: "hass:eye",
simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny",
@@ -83,6 +84,7 @@ export const DOMAINS_WITH_CARD = [
"number",
"scene",
"script",
+ "select",
"timer",
"vacuum",
"water_heater",
@@ -121,6 +123,7 @@ export const DOMAINS_HIDE_MORE_INFO = [
"input_text",
"number",
"scene",
+ "select",
];
/** Domains that should have the history hidden in the more info dialog. */
diff --git a/src/data/select.ts b/src/data/select.ts
new file mode 100644
index 0000000000..9d7058d8ce
--- /dev/null
+++ b/src/data/select.ts
@@ -0,0 +1,25 @@
+import {
+ HassEntityAttributeBase,
+ HassEntityBase,
+} from "home-assistant-js-websocket";
+import { HomeAssistant } from "../types";
+
+interface SelectEntityAttributes extends HassEntityAttributeBase {
+ options: string[];
+}
+
+export interface SelectEntity extends HassEntityBase {
+ attributes: SelectEntityAttributes;
+}
+
+export const setSelectOption = (
+ hass: HomeAssistant,
+ entity: string,
+ option: string
+) =>
+ hass.callService(
+ "select",
+ "select_option",
+ { option },
+ { entity_id: entity }
+ );
diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts
index 955f4741c9..f1b63c717c 100644
--- a/src/panels/lovelace/create-element/create-row-element.ts
+++ b/src/panels/lovelace/create-element/create-row-element.ts
@@ -37,6 +37,7 @@ const LAZY_LOAD_TYPES = {
"input-text-entity": () => import("../entity-rows/hui-input-text-entity-row"),
"lock-entity": () => import("../entity-rows/hui-lock-entity-row"),
"number-entity": () => import("../entity-rows/hui-number-entity-row"),
+ "select-entity": () => import("../entity-rows/hui-select-entity-row"),
"timer-entity": () => import("../entity-rows/hui-timer-entity-row"),
conditional: () => import("../special-rows/hui-conditional-row"),
"weather-entity": () => import("../entity-rows/hui-weather-entity-row"),
@@ -68,6 +69,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
remote: "toggle",
scene: "scene",
script: "script",
+ select: "select",
sensor: "sensor",
timer: "timer",
switch: "toggle",
diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts
new file mode 100644
index 0000000000..7133db16f8
--- /dev/null
+++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts
@@ -0,0 +1,186 @@
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ PropertyValues,
+ TemplateResult,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import { ifDefined } from "lit/directives/if-defined";
+import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
+import { stopPropagation } from "../../../common/dom/stop_propagation";
+import { computeDomain } from "../../../common/entity/compute_domain";
+import { computeStateName } from "../../../common/entity/compute_state_name";
+import "../../../components/entity/state-badge";
+import "../../../components/ha-paper-dropdown-menu";
+import { UNAVAILABLE_STATES } from "../../../data/entity";
+import { forwardHaptic } from "../../../data/haptics";
+import { SelectEntity, setSelectOption } from "../../../data/select";
+import { ActionHandlerEvent } from "../../../data/lovelace";
+import { HomeAssistant } from "../../../types";
+import { EntitiesCardEntityConfig } from "../cards/types";
+import { actionHandler } from "../common/directives/action-handler-directive";
+import { handleAction } from "../common/handle-action";
+import { hasAction } from "../common/has-action";
+import { hasConfigOrEntityChanged } from "../common/has-changed";
+import { createEntityNotFoundWarning } from "../components/hui-warning";
+import { LovelaceRow } from "./types";
+
+@customElement("hui-select-entity-row")
+class HuiSelectEntityRow extends LitElement implements LovelaceRow {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @state() private _config?: EntitiesCardEntityConfig;
+
+ public setConfig(config: EntitiesCardEntityConfig): void {
+ if (!config || !config.entity) {
+ throw new Error("Entity must be specified");
+ }
+
+ this._config = config;
+ }
+
+ protected shouldUpdate(changedProps: PropertyValues): boolean {
+ return hasConfigOrEntityChanged(this, changedProps);
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass || !this._config) {
+ return html``;
+ }
+
+ const stateObj = this.hass.states[this._config.entity] as
+ | SelectEntity
+ | undefined;
+
+ if (!stateObj) {
+ return html`
+
+ ${createEntityNotFoundWarning(this.hass, this._config.entity)}
+
+ `;
+ }
+
+ const pointer =
+ (this._config.tap_action && this._config.tap_action.action !== "none") ||
+ (this._config.entity &&
+ !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
+
+ return html`
+
+
+
+ ${stateObj.attributes.options
+ ? stateObj.attributes.options.map(
+ (option) =>
+ html`
+ ${(stateObj.attributes.device_class &&
+ this.hass!.localize(
+ `component.select.state.${stateObj.attributes.device_class}.${option}`
+ )) ||
+ this.hass!.localize(
+ `component.select.state._.${option}`
+ ) ||
+ option}
+ `
+ )
+ : ""}
+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+
+ if (!this.hass || !this._config) {
+ return;
+ }
+
+ const stateObj = this.hass.states[this._config.entity] as
+ | SelectEntity
+ | undefined;
+
+ if (!stateObj) {
+ return;
+ }
+
+ // Update selected after rendering the items or else it won't work in Firefox
+ if (stateObj.attributes.options) {
+ this.shadowRoot!.querySelector(
+ "paper-listbox"
+ )!.selected = stateObj.attributes.options.indexOf(stateObj.state);
+ }
+ }
+
+ private _handleAction(ev: ActionHandlerEvent) {
+ handleAction(this, this.hass!, this._config!, ev.detail.action!);
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ :host {
+ display: flex;
+ align-items: center;
+ }
+ ha-paper-dropdown-menu {
+ margin-left: 16px;
+ flex: 1;
+ }
+ paper-item {
+ cursor: pointer;
+ min-width: 200px;
+ }
+ .pointer {
+ cursor: pointer;
+ }
+ state-badge:focus {
+ outline: none;
+ background: var(--divider-color);
+ border-radius: 100%;
+ }
+ `;
+ }
+
+ private _selectedChanged(ev): void {
+ const stateObj = this.hass!.states[this._config!.entity];
+ const option = ev.target.selectedItem.option;
+ if (option === stateObj.state) {
+ return;
+ }
+
+ forwardHaptic("light");
+
+ setSelectOption(this.hass!, stateObj.entity_id, option);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-select-entity-row": HuiSelectEntityRow;
+ }
+}
diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js
index 8a365f612a..f3ee74bbf3 100644
--- a/src/state-summary/state-card-content.js
+++ b/src/state-summary/state-card-content.js
@@ -14,6 +14,7 @@ import "./state-card-media_player";
import "./state-card-number";
import "./state-card-scene";
import "./state-card-script";
+import "./state-card-select";
import "./state-card-timer";
import "./state-card-toggle";
import "./state-card-vacuum";
diff --git a/src/state-summary/state-card-select.ts b/src/state-summary/state-card-select.ts
new file mode 100644
index 0000000000..0876f71e41
--- /dev/null
+++ b/src/state-summary/state-card-select.ts
@@ -0,0 +1,99 @@
+import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ PropertyValues,
+ TemplateResult,
+} from "lit";
+import { customElement, property } from "lit/decorators";
+import { stopPropagation } from "../common/dom/stop_propagation";
+import { computeStateName } from "../common/entity/compute_state_name";
+import "../components/entity/state-badge";
+import { SelectEntity, setSelectOption } from "../data/select";
+import type { HomeAssistant } from "../types";
+
+@customElement("state-card-select")
+class StateCardSelect extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public stateObj!: SelectEntity;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+
+ ${this.stateObj.attributes.options.map(
+ (option) =>
+ html`
+ ${(this.stateObj.attributes.device_class &&
+ this.hass.localize(
+ `component.select.state.${this.stateObj.attributes.device_class}.${option}`
+ )) ||
+ this.hass.localize(`component.select.state._.${option}`) ||
+ option}
+ `
+ )}
+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+ if (!changedProps.has("stateObj")) {
+ return;
+ }
+ // Update selected after rendering the items or else it won't work in Firefox
+ this.shadowRoot!.querySelector(
+ "paper-listbox"
+ )!.selected = this.stateObj.attributes.options.indexOf(this.stateObj.state);
+ }
+
+ private _selectedOptionChanged(ev) {
+ const option = ev.target.selectedItem.option;
+ if (option === this.stateObj.state) {
+ return;
+ }
+ setSelectOption(this.hass, this.stateObj.entity_id, option);
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ :host {
+ display: block;
+ }
+
+ state-badge {
+ float: left;
+ margin-top: 10px;
+ }
+
+ paper-dropdown-menu-light {
+ display: block;
+ margin-left: 53px;
+ }
+
+ paper-item {
+ cursor: pointer;
+ min-width: 200px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "state-card-select": StateCardSelect;
+ }
+}