diff --git a/src/common/entity/state_color.ts b/src/common/entity/state_color.ts
index 32d0be425b..efcac098f2 100644
--- a/src/common/entity/state_color.ts
+++ b/src/common/entity/state_color.ts
@@ -1,6 +1,7 @@
/** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity";
+import { computeGroupDomain, GroupEntity } from "../../data/group";
import { computeCssVariable } from "../../resources/css-variables";
import { slugify } from "../string/slugify";
import { batteryStateColorProperty } from "./color/battery_color";
@@ -52,11 +53,11 @@ export const stateColorCss = (stateObj: HassEntity, state?: string) => {
};
export const domainStateColorProperties = (
+ domain: string,
stateObj: HassEntity,
state?: string
): string[] => {
const compareState = state !== undefined ? state : stateObj.state;
- const domain = computeDomain(stateObj.entity_id);
const active = stateActive(stateObj, state);
const properties: string[] = [];
@@ -95,8 +96,16 @@ export const stateColorProperties = (
}
}
+ // Special rules for group coloring
+ if (domain === "group") {
+ const groupDomain = computeGroupDomain(stateObj as GroupEntity);
+ if (groupDomain && STATE_COLORED_DOMAIN.has(groupDomain)) {
+ return domainStateColorProperties(groupDomain, stateObj, state);
+ }
+ }
+
if (STATE_COLORED_DOMAIN.has(domain)) {
- return domainStateColorProperties(stateObj, state);
+ return domainStateColorProperties(domain, stateObj, state);
}
return undefined;
diff --git a/src/components/ha-control-slider.ts b/src/components/ha-control-slider.ts
index 6b31e6d2e6..2cc172969a 100644
--- a/src/components/ha-control-slider.ts
+++ b/src/components/ha-control-slider.ts
@@ -244,6 +244,7 @@ export class HaControlSlider extends LitElement {
})}
>
+
${this.mode === "cursor"
? this.value != null
? html`
@@ -310,6 +311,13 @@ export class HaControlSlider extends LitElement {
background: var(--control-slider-background);
opacity: var(--control-slider-background-opacity);
}
+ ::slotted([slot="background"]) {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ }
.slider .slider-track-bar {
--border-radius: var(--control-slider-border-radius);
--handle-size: 4px;
@@ -424,6 +432,7 @@ export class HaControlSlider extends LitElement {
bottom: 0;
left: calc(var(--value, 0) * (100% - var(--cursor-size)));
width: var(--cursor-size);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.slider .slider-track-cursor:after {
height: 50%;
diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts
index dc082e0b63..7e3fae87aa 100644
--- a/src/data/entity_attributes.ts
+++ b/src/data/entity_attributes.ts
@@ -1,4 +1,5 @@
export const STATE_ATTRIBUTES = [
+ "entity_id",
"assumed_state",
"attribution",
"custom_ui_more_info",
diff --git a/src/dialogs/more-info/components/cover/ha-more-info-cover-buttons.ts b/src/dialogs/more-info/components/cover/ha-more-info-cover-buttons.ts
new file mode 100644
index 0000000000..99ee617ecb
--- /dev/null
+++ b/src/dialogs/more-info/components/cover/ha-more-info-cover-buttons.ts
@@ -0,0 +1,311 @@
+import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ nothing,
+ TemplateResult,
+} from "lit";
+import { customElement, property } from "lit/decorators";
+import { repeat } from "lit/directives/repeat";
+import memoizeOne from "memoize-one";
+import {
+ computeCloseIcon,
+ computeOpenIcon,
+} from "../../../../common/entity/cover_icon";
+import { supportsFeature } from "../../../../common/entity/supports-feature";
+import "../../../../components/ha-control-button";
+import "../../../../components/ha-control-button-group";
+import "../../../../components/ha-control-slider";
+import "../../../../components/ha-svg-icon";
+import {
+ canClose,
+ canCloseTilt,
+ canOpen,
+ canOpenTilt,
+ canStop,
+ canStopTilt,
+ CoverEntity,
+ CoverEntityFeature,
+} from "../../../../data/cover";
+import { HomeAssistant } from "../../../../types";
+
+type CoverButton =
+ | "open"
+ | "close"
+ | "stop"
+ | "open-tilt"
+ | "close-tilt"
+ | "none";
+
+type CoverLayout = {
+ type: "line" | "cross";
+ buttons: CoverButton[];
+};
+
+export const getCoverLayout = memoizeOne(
+ (stateObj: CoverEntity): CoverLayout => {
+ const supportsOpen = supportsFeature(stateObj, CoverEntityFeature.OPEN);
+ const supportsClose = supportsFeature(stateObj, CoverEntityFeature.CLOSE);
+ const supportsStop = supportsFeature(stateObj, CoverEntityFeature.STOP);
+ const supportsOpenTilt = supportsFeature(
+ stateObj,
+ CoverEntityFeature.OPEN_TILT
+ );
+ const supportsCloseTilt = supportsFeature(
+ stateObj,
+ CoverEntityFeature.CLOSE_TILT
+ );
+ const supportsStopTilt = supportsFeature(
+ stateObj,
+ CoverEntityFeature.STOP_TILT
+ );
+
+ if (
+ (supportsOpen || supportsClose) &&
+ (supportsOpenTilt || supportsCloseTilt)
+ ) {
+ return {
+ type: "cross",
+ buttons: [
+ supportsOpen ? "open" : "none",
+ supportsCloseTilt ? "close-tilt" : "none",
+ supportsStop || supportsStopTilt ? "stop" : "none",
+ supportsOpenTilt ? "open-tilt" : "none",
+ supportsClose ? "close" : "none",
+ ],
+ };
+ }
+
+ if (supportsOpen || supportsClose) {
+ const buttons: CoverButton[] = [];
+ if (supportsOpen) buttons.push("open");
+ if (supportsStop) buttons.push("stop");
+ if (supportsClose) buttons.push("close");
+ return {
+ type: "line",
+ buttons,
+ };
+ }
+
+ if (supportsOpenTilt || supportsCloseTilt) {
+ const buttons: CoverButton[] = [];
+ if (supportsOpenTilt) buttons.push("open-tilt");
+ if (supportsStopTilt) buttons.push("stop");
+ if (supportsCloseTilt) buttons.push("close-tilt");
+ return {
+ type: "line",
+ buttons,
+ };
+ }
+
+ return {
+ type: "line",
+ buttons: [],
+ };
+ }
+);
+
+@customElement("ha-more-info-cover-buttons")
+export class HaMoreInfoCoverButtons extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: CoverEntity;
+
+ private _onOpenTap(ev): void {
+ ev.stopPropagation();
+ this.hass!.callService("cover", "open_cover", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ private _onCloseTap(ev): void {
+ ev.stopPropagation();
+ this.hass!.callService("cover", "close_cover", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ private _onOpenTiltTap(ev): void {
+ ev.stopPropagation();
+ this.hass!.callService("cover", "open_cover_tilt", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ private _onCloseTiltTap(ev): void {
+ ev.stopPropagation();
+ this.hass!.callService("cover", "close_cover_tilt", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ private _onStopTap(ev): void {
+ ev.stopPropagation();
+ if (supportsFeature(this.stateObj, CoverEntityFeature.STOP)) {
+ this.hass!.callService("cover", "stop_cover", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+ if (supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT)) {
+ this.hass!.callService("cover", "stop_cover_tilt", {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+ }
+
+ protected renderButton(button: CoverButton | undefined) {
+ if (button === "open") {
+ return html`
+
+
+
+ `;
+ }
+ if (button === "close") {
+ return html`
+
+
+
+ `;
+ }
+ if (button === "stop") {
+ return html`
+
+
+
+ `;
+ }
+ if (button === "open-tilt") {
+ return html`
+
+
+
+ `;
+ }
+ if (button === "close-tilt") {
+ return html`
+
+
+
+ `;
+ }
+ return nothing;
+ }
+
+ protected render(): TemplateResult {
+ const layout = getCoverLayout(this.stateObj);
+
+ return html`
+ ${layout.type === "line"
+ ? html`
+
+ ${repeat(
+ layout.buttons,
+ (action) => action,
+ (action) => this.renderButton(action)
+ )}
+
+ `
+ : nothing}
+ ${layout.type === "cross"
+ ? html`
+
+ ${repeat(
+ layout.buttons,
+ (action) => action,
+ (action) => this.renderButton(action)
+ )}
+
+ `
+ : nothing}
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-control-button-group {
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ --control-button-group-spacing: 6px;
+ --control-button-group-thickness: 100px;
+ }
+ .cross-container {
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: repeat(3, min(100px, 25vw, 15vh));
+ grid-template-rows: repeat(3, min(100px, 25vw, 15vh));
+ grid-template-areas: ". open ." "close-tilt stop open-tilt" ". close .";
+ }
+ .cross-container > * {
+ width: 100%;
+ height: 100%;
+ }
+ .cross-container > [data-button="open"] {
+ grid-area: open;
+ }
+ .cross-container > [data-button="close"] {
+ grid-area: close;
+ }
+ .cross-container > [data-button="open-tilt"] {
+ grid-area: open-tilt;
+ }
+ .cross-container > [data-button="close-tilt"] {
+ grid-area: close-tilt;
+ }
+ .cross-container > [data-button="stop"] {
+ grid-area: stop;
+ }
+ ha-control-button {
+ --control-button-border-radius: 18px;
+ --mdc-icon-size: 24px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-more-info-cover-buttons": HaMoreInfoCoverButtons;
+ }
+}
diff --git a/src/dialogs/more-info/components/cover/ha-more-info-cover-position.ts b/src/dialogs/more-info/components/cover/ha-more-info-cover-position.ts
new file mode 100644
index 0000000000..ea30c823c6
--- /dev/null
+++ b/src/dialogs/more-info/components/cover/ha-more-info-cover-position.ts
@@ -0,0 +1,87 @@
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
+import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
+import { stateColorCss } from "../../../../common/entity/state_color";
+import "../../../../components/ha-control-slider";
+import { CoverEntity } from "../../../../data/cover";
+import { UNAVAILABLE } from "../../../../data/entity";
+import { HomeAssistant } from "../../../../types";
+
+@customElement("ha-more-info-cover-position")
+export class HaMoreInfoCoverPosition extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: CoverEntity;
+
+ @state() value?: number;
+
+ protected updated(changedProp: Map): void {
+ if (changedProp.has("stateObj")) {
+ const currentPosition = this.stateObj?.attributes.current_position;
+ this.value =
+ currentPosition != null ? Math.round(currentPosition) : undefined;
+ }
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ const value = (ev.detail as any).value;
+ if (isNaN(value)) return;
+
+ this.hass.callService("cover", "set_cover_position", {
+ entity_id: this.stateObj!.entity_id,
+ position: value,
+ });
+ }
+
+ protected render(): TemplateResult {
+ const color = stateColorCss(this.stateObj);
+
+ return html`
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-control-slider {
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ /* Force inactive state to be colored for the slider */
+ --state-cover-inactive-color: var(--state-cover-active-color);
+ --control-slider-thickness: 100px;
+ --control-slider-border-radius: 24px;
+ --control-slider-color: var(--primary-color);
+ --control-slider-background: var(--disabled-color);
+ --control-slider-background-opacity: 0.2;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-more-info-cover-position": HaMoreInfoCoverPosition;
+ }
+}
diff --git a/src/dialogs/more-info/components/cover/ha-more-info-cover-tilt-position.ts b/src/dialogs/more-info/components/cover/ha-more-info-cover-tilt-position.ts
new file mode 100644
index 0000000000..b9b52d65de
--- /dev/null
+++ b/src/dialogs/more-info/components/cover/ha-more-info-cover-tilt-position.ts
@@ -0,0 +1,128 @@
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ TemplateResult,
+ unsafeCSS,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
+import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
+import { stateColorCss } from "../../../../common/entity/state_color";
+import "../../../../components/ha-control-slider";
+import { CoverEntity } from "../../../../data/cover";
+import { UNAVAILABLE } from "../../../../data/entity";
+import { HomeAssistant } from "../../../../types";
+
+function generateTiltSliderTrackBackgroundGradient() {
+ const count = 24;
+ const minStrokeWidth = 0.2;
+ const gradient: [number, string][] = [];
+
+ for (let i = 0; i < count; i++) {
+ const stopOffset1 = i / count;
+ const stopOffset2 =
+ stopOffset1 +
+ (i / count ** 2) * (1 - minStrokeWidth) +
+ minStrokeWidth / count;
+
+ if (i !== 0) {
+ gradient.push([stopOffset1, "transparent"]);
+ }
+ gradient.push([stopOffset1, "var(--control-slider-color)"]);
+ gradient.push([stopOffset2, "var(--control-slider-color)"]);
+ gradient.push([stopOffset2, "transparent"]);
+ }
+
+ return unsafeCSS(
+ gradient
+ .map(([stop, color]) => `${color} ${(stop as number) * 100}%`)
+ .join(", ")
+ );
+}
+
+const GRADIENT = generateTiltSliderTrackBackgroundGradient();
+
+@customElement("ha-more-info-cover-tilt-position")
+export class HaMoreInfoCoverTiltPosition extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: CoverEntity;
+
+ @state() value?: number;
+
+ protected updated(changedProp: Map): void {
+ if (changedProp.has("stateObj")) {
+ this.value =
+ this.stateObj.attributes.current_tilt_position != null
+ ? Math.round(this.stateObj.attributes.current_tilt_position)
+ : undefined;
+ }
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ const value = (ev.detail as any).value;
+ if (isNaN(value)) return;
+
+ this.hass.callService("cover", "set_cover_tilt_position", {
+ entity_id: this.stateObj!.entity_id,
+ tilt_position: value,
+ });
+ }
+
+ protected render(): TemplateResult {
+ const color = stateColorCss(this.stateObj);
+ const isUnavailable = this.stateObj.state === UNAVAILABLE;
+
+ return html`
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-control-slider {
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ /* Force inactive state to be colored for the slider */
+ --state-cover-inactive-color: var(--state-cover-active-color);
+ --control-slider-thickness: 100px;
+ --control-slider-border-radius: 24px;
+ --control-slider-color: var(--primary-color);
+ --control-slider-background-opacity: 0.2;
+ --control-slider-background: var(--control-slider-color);
+ }
+ .gradient {
+ background: -webkit-linear-gradient(top, ${GRADIENT});
+ opacity: 0.6;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-more-info-cover-tilt-position": HaMoreInfoCoverTiltPosition;
+ }
+}
diff --git a/src/dialogs/more-info/components/cover/ha-more-info-cover-toggle.ts b/src/dialogs/more-info/components/cover/ha-more-info-cover-toggle.ts
new file mode 100644
index 0000000000..0ee9b54172
--- /dev/null
+++ b/src/dialogs/more-info/components/cover/ha-more-info-cover-toggle.ts
@@ -0,0 +1,169 @@
+import { HassEntity } from "home-assistant-js-websocket";
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import { styleMap } from "lit/directives/style-map";
+import { domainIcon } from "../../../../common/entity/domain_icon";
+import { stateColorCss } from "../../../../common/entity/state_color";
+import "../../../../components/ha-control-button";
+import "../../../../components/ha-control-switch";
+import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity";
+import { forwardHaptic } from "../../../../data/haptics";
+import { HomeAssistant } from "../../../../types";
+
+@customElement("ha-more-info-cover-toggle")
+export class HaMoreInfoCoverToggle extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: HassEntity;
+
+ private _valueChanged(ev) {
+ const checked = ev.target.checked as boolean;
+
+ if (checked) {
+ this._turnOn();
+ } else {
+ this._turnOff();
+ }
+ }
+
+ private _turnOn() {
+ this._callService(true);
+ }
+
+ private _turnOff() {
+ this._callService(false);
+ }
+
+ private async _callService(turnOn): Promise {
+ if (!this.hass || !this.stateObj) {
+ return;
+ }
+ forwardHaptic("light");
+
+ await this.hass.callService(
+ "cover",
+ turnOn ? "open_cover" : "close_cover",
+ {
+ entity_id: this.stateObj.entity_id,
+ }
+ );
+ }
+
+ protected render(): TemplateResult {
+ const onColor = stateColorCss(this.stateObj, "open");
+ const offColor = stateColorCss(this.stateObj, "closed");
+
+ const isOn =
+ this.stateObj.state === "open" ||
+ this.stateObj.state === "closing" ||
+ this.stateObj.state === "opening";
+ const isOff = this.stateObj.state === "closed";
+
+ if (
+ this.stateObj.attributes.assumed_state ||
+ this.stateObj.state === UNKNOWN
+ ) {
+ return html`
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ return html`
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-control-switch {
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ --control-switch-thickness: 100px;
+ --control-switch-border-radius: 24px;
+ --control-switch-padding: 6px;
+ --mdc-icon-size: 24px;
+ }
+ .buttons {
+ display: flex;
+ flex-direction: column;
+ width: 100px;
+ height: 45vh;
+ max-height: 320px;
+ min-height: 200px;
+ padding: 6px;
+ box-sizing: border-box;
+ }
+ ha-control-button {
+ flex: 1;
+ width: 100%;
+ --control-button-border-radius: 18px;
+ --mdc-icon-size: 24px;
+ }
+ ha-control-button.active {
+ --control-button-icon-color: white;
+ --control-button-background-color: var(--color);
+ --control-button-background-opacity: 1;
+ }
+ ha-control-button:not(:last-child) {
+ margin-bottom: 6px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-more-info-cover-toggle": HaMoreInfoCoverToggle;
+ }
+}
diff --git a/src/dialogs/more-info/components/ha-more-info-control-style.ts b/src/dialogs/more-info/components/ha-more-info-control-style.ts
index 81032af738..5f33a1a53d 100644
--- a/src/dialogs/more-info/components/ha-more-info-control-style.ts
+++ b/src/dialogs/more-info/components/ha-more-info-control-style.ts
@@ -14,6 +14,10 @@ export const moreInfoControlStyle = css`
align-items: center;
}
+ .controls:not(:last-child) {
+ margin-bottom: 24px;
+ }
+
.controls > *:not(:last-child) {
margin-bottom: 24px;
}
diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts
index 9454bc39aa..e6f48400d2 100644
--- a/src/dialogs/more-info/const.ts
+++ b/src/dialogs/more-info/const.ts
@@ -17,6 +17,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
/** Domains with with new more info design. */
export const DOMAINS_WITH_NEW_MORE_INFO = [
+ "cover",
"fan",
"input_boolean",
"light",
diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts
index 2b01a2fcad..659862a9b1 100644
--- a/src/dialogs/more-info/controls/more-info-cover.ts
+++ b/src/dialogs/more-info/controls/more-info-cover.ts
@@ -1,89 +1,195 @@
-import { css, CSSResult, html, LitElement, nothing } from "lit";
-import { customElement, property } from "lit/decorators";
-import { attributeClassNames } from "../../../common/entity/attribute_class_names";
+import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import {
- FeatureClassNames,
- featureClassNames,
-} from "../../../common/entity/feature_class_names";
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ nothing,
+ PropertyValues,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
+import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import "../../../components/ha-attributes";
-import "../../../components/ha-cover-tilt-controls";
-import "../../../components/ha-labeled-slider";
-import {
- CoverEntity,
- CoverEntityFeature,
- isTiltOnly,
-} from "../../../data/cover";
-import { HomeAssistant } from "../../../types";
-
-export const FEATURE_CLASS_NAMES: FeatureClassNames = {
- [CoverEntityFeature.SET_POSITION]: "has-set_position",
- [CoverEntityFeature.OPEN_TILT]: "has-open_tilt",
- [CoverEntityFeature.CLOSE_TILT]: "has-close_tilt",
- [CoverEntityFeature.STOP_TILT]: "has-stop_tilt",
- [CoverEntityFeature.SET_TILT_POSITION]: "has-set_tilt_position",
-};
+import { CoverEntity, CoverEntityFeature } from "../../../data/cover";
+import type { HomeAssistant } from "../../../types";
+import "../components/cover/ha-more-info-cover-buttons";
+import "../components/cover/ha-more-info-cover-position";
+import "../components/cover/ha-more-info-cover-tilt-position";
+import "../components/cover/ha-more-info-cover-toggle";
+import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
+import "../components/ha-more-info-state-header";
@customElement("more-info-cover")
class MoreInfoCover extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @property({ attribute: false }) public stateObj!: CoverEntity;
+ @property({ attribute: false }) public stateObj?: CoverEntity;
+
+ @state() private _displayedPosition?: number;
+
+ @state() private _mode?: "position" | "button";
+
+ private _toggleMode() {
+ this._mode = this._mode === "position" ? "button" : "position";
+ }
+
+ private _positionChanged(ev) {
+ const value = (ev.detail as any).value;
+ if (isNaN(value)) return;
+ this._displayedPosition = value;
+ }
+
+ protected willUpdate(changedProps: PropertyValues): void {
+ super.willUpdate(changedProps);
+ if (changedProps.has("stateObj") && this.stateObj) {
+ if (supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION)) {
+ const currentPosition = this.stateObj?.attributes.current_position;
+ this._displayedPosition =
+ currentPosition != null ? Math.round(currentPosition) : undefined;
+ }
+ if (!this._mode) {
+ this._mode =
+ supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
+ supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)
+ ? "position"
+ : "button";
+ }
+ }
+ }
+
+ private get _stateOverride() {
+ if (this._displayedPosition == null) return undefined;
+
+ const tempState = {
+ ...this.stateObj,
+ state: this._displayedPosition ? "open" : "closed",
+ attributes: {
+ ...this.stateObj!.attributes,
+ current_position: this._displayedPosition,
+ },
+ } as CoverEntity;
+
+ const stateDisplay = computeStateDisplay(
+ this.hass.localize,
+ tempState!,
+ this.hass.locale,
+ this.hass.entities
+ );
+
+ return this._displayedPosition && this._displayedPosition !== 100
+ ? `${stateDisplay} - ${Math.round(
+ this._displayedPosition
+ )}${blankBeforePercent(this.hass!.locale)}%`
+ : stateDisplay;
+ }
protected render() {
- if (!this.stateObj) {
+ if (!this.hass || !this.stateObj) {
return nothing;
}
- const _isTiltOnly = isTiltOnly(this.stateObj);
+ const supportsPosition = supportsFeature(
+ this.stateObj,
+ CoverEntityFeature.SET_POSITION
+ );
+
+ const supportsTiltPosition = supportsFeature(
+ this.stateObj,
+ CoverEntityFeature.SET_TILT_POSITION
+ );
+
+ const supportsOpenClose =
+ supportsFeature(this.stateObj, CoverEntityFeature.OPEN) ||
+ supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) ||
+ supportsFeature(this.stateObj, CoverEntityFeature.STOP);
+
+ const supportsTilt =
+ supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) ||
+ supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
+ supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
+
+ const supportsOpenCloseWithoutStop =
+ supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
+ supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
+ !supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
+ !supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
+ !supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
return html`
-
-
-
-
-
-
- ${supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)
- ? // Either render the labeled slider and put the tilt buttons into its slot
- // or (if tilt position is not supported and therefore no slider is shown)
- // render a title
(same style as for a labeled slider) and directly put
- // the tilt controls on the more-info.
- html`
- ${!_isTiltOnly
- ? html` `
- : nothing}
- `
- : !_isTiltOnly
- ? html`
-
- ${this.hass.localize("ui.card.cover.tilt_position")}
-
-
- `
- : nothing}
+
+
+
+ ${
+ this._mode === "position"
+ ? html`
+ ${supportsPosition
+ ? html`
+
+ `
+ : nothing}
+ ${supportsTiltPosition
+ ? html`
+
+ `
+ : nothing}
+ `
+ : nothing
+ }
+ ${
+ this._mode === "button"
+ ? html`
+ ${supportsOpenCloseWithoutStop
+ ? html`
+
+ `
+ : supportsOpenClose || supportsTilt
+ ? html`
+
+ `
+ : nothing}
+ `
+ : nothing
+ }
+
+ ${
+ (supportsPosition || supportsTiltPosition) &&
+ (supportsOpenClose || supportsTilt)
+ ? html`
+
+
+
+ `
+ : nothing
+ }
* {
+ margin: 0 8px;
+ }
+ `,
];
- return classes.join(" ");
- }
-
- private _coverPositionSliderChanged(ev) {
- this.hass.callService("cover", "set_cover_position", {
- entity_id: this.stateObj.entity_id,
- position: ev.target.value,
- });
- }
-
- private _coverTiltPositionSliderChanged(ev) {
- this.hass.callService("cover", "set_cover_tilt_position", {
- entity_id: this.stateObj.entity_id,
- tilt_position: ev.target.value,
- });
- }
-
- static get styles(): CSSResult {
- return css`
- .current_position,
- .tilt {
- max-height: 0px;
- overflow: hidden;
- }
-
- .has-set_position .current_position,
- .has-current_position .current_position,
- .has-open_tilt .tilt,
- .has-close_tilt .tilt,
- .has-stop_tilt .tilt,
- .has-set_tilt_position .tilt,
- .has-current_tilt_position .tilt {
- max-height: 208px;
- }
-
- /* from ha-labeled-slider for consistent look */
- .title {
- margin: 5px 0 8px;
- color: var(--primary-text-color);
- }
- `;
}
}
diff --git a/src/dialogs/more-info/controls/more-info-group.ts b/src/dialogs/more-info/controls/more-info-group.ts
index 2e2b069e68..0f5131c395 100644
--- a/src/dialogs/more-info/controls/more-info-group.ts
+++ b/src/dialogs/more-info/controls/more-info-group.ts
@@ -59,6 +59,7 @@ class MoreInfoGroup extends LitElement {
attributes: {
...baseStateObj.attributes,
friendly_name: this.stateObj.attributes.friendly_name,
+ entity_id: this.stateObj.attributes.entity_id,
},
};
const type = domainMoreInfoType(groupDomain);
diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts
index 0628b3dc59..3bb765c1cd 100644
--- a/src/resources/ha-style.ts
+++ b/src/resources/ha-style.ts
@@ -96,8 +96,8 @@ documentContainer.innerHTML = `
--disabled-color: #bdbdbd;
--red-color: #f44336;
--pink-color: #e91e63;
- --purple-color: #9c27b0;
- --deep-purple-color: #673ab7;
+ --purple-color: #926bc7;
+ --deep-purple-color: #6e41ab;
--indigo-color: #3f51b5;
--blue-color: #2196f3;
--light-blue-color: #03a9f4;
diff --git a/src/translations/en.json b/src/translations/en.json
index 4c4d1bdf74..eef6d37035 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -891,7 +891,11 @@
"close_cover": "Close cover",
"open_tilt_cover": "Open cover tilt",
"close_tilt_cover": "Close cover tilt",
- "stop_cover": "Stop cover"
+ "stop_cover": "Stop cover",
+ "switch_mode": {
+ "button": "Switch to button mode",
+ "position": "Switch to position mode"
+ }
},
"zone": {
"graph_unit": "People home"