mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-24 21:37:21 +00:00
Add Select entity (#9422)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
cecb66451c
commit
c1d571de42
@ -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. */
|
||||
|
25
src/data/select.ts
Normal file
25
src/data/select.ts
Normal file
@ -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 }
|
||||
);
|
@ -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",
|
||||
|
186
src/panels/lovelace/entity-rows/hui-select-entity-row.ts
Normal file
186
src/panels/lovelace/entity-rows/hui-select-entity-row.ts
Normal file
@ -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`
|
||||
<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.entity)}
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${this._config.icon}
|
||||
.overrideImage=${this._config.image}
|
||||
class=${classMap({
|
||||
pointer,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(stateObj.attributes.device_class &&
|
||||
this.hass!.localize(
|
||||
`component.select.state.${stateObj.attributes.device_class}.${option}`
|
||||
)) ||
|
||||
this.hass!.localize(
|
||||
`component.select.state._.${option}`
|
||||
) ||
|
||||
option}</paper-item
|
||||
>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
99
src/state-summary/state-card-select.ts
Normal file
99
src/state-summary/state-card-select.ts
Normal file
@ -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`
|
||||
<state-badge .stateObj=${this.stateObj}></state-badge>
|
||||
<paper-dropdown-menu-light
|
||||
.label=${computeStateName(this.stateObj)}
|
||||
@iron-select=${this._selectedOptionChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${this.stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(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}</paper-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user