diff --git a/package.json b/package.json index 8275958cd7..f6b52875dd 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@material/mwc-textfield": "^0.27.0", "@material/mwc-top-app-bar-fixed": "^0.27.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", + "@material/web": "^1.0.0-pre.1", "@mdi/js": "7.1.96", "@mdi/svg": "7.1.96", "@polymer/app-layout": "^3.1.0", diff --git a/src/components/ha-bar-slider.ts b/src/components/ha-bar-slider.ts index 51bd09df6a..9142b35e72 100644 --- a/src/components/ha-bar-slider.ts +++ b/src/components/ha-bar-slider.ts @@ -44,7 +44,7 @@ const getPercentageFromEvent = (e: HammerInput, vertical: boolean) => { @customElement("ha-bar-slider") export class HaBarSlider extends LitElement { - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) public disabled = false; @property() @@ -245,14 +245,16 @@ export class HaBarSlider extends LitElement { >
${this.mode === "cursor" - ? html` -
- ` + ? this.value != null + ? html` +
+ ` + : null : html`
string | TemplateResult; + + @property({ type: Number }) + public value?: number; + + @property({ type: Number }) + public step = 1; + + @property({ type: Number }) + public min = 0; + + @property({ type: Number }) + public max = 100; + + @property({ type: Boolean, attribute: "show-handle" }) + public showHandle = false; + + @property() public label = ""; + + @property({ type: Boolean, attribute: "show-label" }) + public showLabel = false; + + @property({ type: Boolean, attribute: "show-value" }) + public showValue = false; + + protected render(): TemplateResult { + return html` +
+ ${this.showValue + ? html` +

+ ${this.valueFormatter + ? this.valueFormatter(this.value) + : this.value} +

+ ` + : null} + + + ${this.showLabel ? html`

${this.label}

` : null} +
+ `; + } + + static get styles(): CSSResultGroup { + return css` + .container { + display: flex; + align-items: center; + flex-direction: column; + } + ha-bar-slider { + height: var(--more-info-slider-bar-height, 330px); + --slider-bar-thickness: 100px; + --slider-bar-border-radius: 24px; + --slider-bar-color: var( + --more-info-slider-bar-color, + var(--primary-color) + ); + --slider-bar-background: var( + --more-info-slider-bar-background, + var(--disabled-color) + ); + --slider-bar-background-opacity: var( + --more-info-slider-bar-background-opacity, + 0.2 + ); + } + p { + font-weight: 500; + font-size: 14px; + line-height: 20px; + text-align: center; + margin: 16px 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-bar-slider": HaTileSlider; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-light-brightness.ts b/src/dialogs/more-info/components/ha-more-info-light-brightness.ts new file mode 100644 index 0000000000..83e40781ac --- /dev/null +++ b/src/dialogs/more-info/components/ha-more-info-light-brightness.ts @@ -0,0 +1,111 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { stateActive } from "../../../common/entity/state_active"; +import { stateColorCss } from "../../../common/entity/state_color"; +import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; +import { LightEntity } from "../../../data/light"; +import { HomeAssistant } from "../../../types"; +import "./ha-more-info-bar-slider"; + +@customElement("ha-more-info-light-brightness") +export class HaMoreInfoLightBrightness extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LightEntity; + + @state() value?: number; + + protected updated(changedProp: Map): void { + if (changedProp.has("stateObj")) { + this.value = + this.stateObj.attributes.brightness != null + ? Math.max( + Math.round((this.stateObj.attributes.brightness * 100) / 255), + 1 + ) + : undefined; + } + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + brightness_pct: value, + }); + } + + private _sliderMoved(ev: CustomEvent) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + + this.value = value; + } + + private _valueFormatter = (value?: number) => { + if (value == null) + return computeStateDisplay( + this.hass.localize, + this.stateObj, + this.hass.locale, + this.hass.entities + ); + return `${Math.round(value)}${blankBeforePercent(this.hass!.locale)}%`; + }; + + protected render(): TemplateResult { + let color = stateColorCss(this.stateObj); + + if (this.stateObj.attributes.rgb_color) { + const hsvColor = rgb2hsv(this.stateObj.attributes.rgb_color); + + // Modify the real rgb color for better contrast + if (hsvColor[1] < 0.4) { + // Special case for very light color (e.g: white) + if (hsvColor[1] < 0.1) { + hsvColor[2] = 225; + } else { + hsvColor[1] = 0.4; + } + } + color = rgb2hex(hsv2rgb(hsvColor)); + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-more-info-bar-slider { + --more-info-slider-bar-height: 250px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-light-brightness": HaMoreInfoLightBrightness; + } +} diff --git a/src/dialogs/more-info/components/lights/dialog-light-color-picker.ts b/src/dialogs/more-info/components/lights/dialog-light-color-picker.ts new file mode 100644 index 0000000000..535adf1c3b --- /dev/null +++ b/src/dialogs/more-info/components/lights/dialog-light-color-picker.ts @@ -0,0 +1,618 @@ +import "@material/mwc-button"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import { mdiPalette } from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button-toggle-group"; +import "../../../../components/ha-color-picker"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; +import "../../../../components/ha-icon-button-prev"; +import "../../../../components/ha-labeled-slider"; +import { + getLightCurrentModeRgbColor, + LightColorMode, + LightEntity, + lightSupportsColor, + lightSupportsColorMode, +} from "../../../../data/light"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import "../ha-more-info-bar-slider"; +import { LightColorPickerDialogParams } from "./show-dialog-light-color-picker"; + +type Mode = "color_temp" | "color" | "white"; + +@customElement("dialog-light-color-picker") +class DialogLightColorPicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: LightColorPickerDialogParams; + + @state() private _ctSliderValue?: number; + + @state() private _cwSliderValue?: number; + + @state() private _wwSliderValue?: number; + + @state() private _wvSliderValue?: number; + + @state() private _colorBrightnessSliderValue?: number; + + @state() private _brightnessAdjusted?: number; + + @state() private _hueSegments = 24; + + @state() private _saturationSegments = 8; + + @state() private _colorPickerColor?: [number, number, number]; + + @state() private _mode?: Mode; + + @state() private _modes: Mode[] = []; + + get stateObj() { + return this._params + ? (this.hass.states[this._params.entityId] as LightEntity) + : undefined; + } + + public async showDialog(params: LightColorPickerDialogParams): Promise { + this._params = params; + + const supportsTemp = lightSupportsColorMode( + this.stateObj!, + LightColorMode.COLOR_TEMP + ); + const supportsWhite = lightSupportsColorMode( + this.stateObj!, + LightColorMode.WHITE + ); + + const modes: Mode[] = []; + modes.push("color"); + if (supportsTemp) { + modes.push("color_temp"); + } + if (supportsWhite) { + modes.push("white"); + } + + this._modes = modes; + this._mode = + this.stateObj!.attributes.color_mode === "color_temp" || + this.stateObj!.attributes.color_mode === "white" + ? this.stateObj!.attributes.color_mode + : "color"; + + this._updateSliderValues(); + await this.updateComplete; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params || !this.stateObj) { + return html``; + } + + const supportsTemp = lightSupportsColorMode( + this.stateObj, + LightColorMode.COLOR_TEMP + ); + + const supportsWhite = lightSupportsColorMode( + this.stateObj, + LightColorMode.WHITE + ); + + const supportsRgbww = lightSupportsColorMode( + this.stateObj, + LightColorMode.RGBWW + ); + + const supportsRgbw = + !supportsRgbww && + lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); + + const supportsColor = + supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); + + return html` + +
+ + + + Change Color + +
+
+ ${supportsColor && (supportsTemp || supportsWhite) + ? html` + + ${this._modes.map( + (value) => + html`` + )} + + ` + : ""} +
+ ${supportsTemp && + ((!supportsColor && !supportsWhite) || + this._mode === LightColorMode.COLOR_TEMP) + ? html` + + + ` + : ""} + ${supportsColor && + ((!supportsTemp && !supportsWhite) || this._mode === "color") + ? html` +
+ + + +
+ + ${supportsRgbw || supportsRgbww + ? html`` + : ""} + ${supportsRgbw + ? html` + + ` + : ""} + ${supportsRgbww + ? html` + + + ` + : ""} + ` + : ""} +
+
+
+ `; + } + + public _updateSliderValues() { + const stateObj = this.stateObj; + + if (stateObj?.state === "on") { + this._brightnessAdjusted = undefined; + if ( + stateObj.attributes.color_mode === LightColorMode.RGB && + !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && + !lightSupportsColorMode(stateObj, LightColorMode.RGBW) + ) { + const maxVal = Math.max(...stateObj.attributes.rgb_color!); + + if (maxVal < 255) { + this._brightnessAdjusted = maxVal; + } + } + this._ctSliderValue = stateObj.attributes.color_temp_kelvin; + this._wvSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBW + ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) + : undefined; + this._cwSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) + : undefined; + this._wwSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) + : undefined; + + const currentRgbColor = getLightCurrentModeRgbColor(stateObj); + + this._colorBrightnessSliderValue = currentRgbColor + ? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255) + : undefined; + + this._colorPickerColor = currentRgbColor?.slice(0, 3) as [ + number, + number, + number + ]; + } else { + this._colorPickerColor = [0, 0, 0]; + this._ctSliderValue = undefined; + this._wvSliderValue = undefined; + this._cwSliderValue = undefined; + this._wwSliderValue = undefined; + } + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!changedProps.has("hass")) { + return; + } + + this._updateSliderValues(); + } + + private _handleTabChanged(ev: CustomEvent): void { + const newMode = this._modes[ev.detail.index]; + if (newMode === this._mode) { + return; + } + this._mode = newMode; + } + + private _ctSliderChanged(ev: CustomEvent) { + const ct = ev.detail.value; + + if (isNaN(ct)) { + return; + } + + this._ctSliderValue = ct; + + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + color_temp_kelvin: ct, + }); + } + + private _wvSliderChanged(ev: CustomEvent) { + const target = ev.target as any; + let wv = Number(target.value); + const name = target.name; + + if (isNaN(wv)) { + return; + } + + if (name === "wv") { + this._wvSliderValue = wv; + } else if (name === "cw") { + this._cwSliderValue = wv; + } else if (name === "ww") { + this._wwSliderValue = wv; + } + + wv = Math.min(255, Math.round((wv * 255) / 100)); + + const rgb = getLightCurrentModeRgbColor(this.stateObj!); + + if (name === "wv") { + const rgbw_color = rgb || [0, 0, 0, 0]; + rgbw_color[3] = wv; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbw_color, + }); + return; + } + + const rgbww_color = rgb || [0, 0, 0, 0, 0]; + while (rgbww_color.length < 5) { + rgbww_color.push(0); + } + rgbww_color[name === "cw" ? 3 : 4] = wv; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbww_color, + }); + } + + private _colorBrightnessSliderChanged(ev: CustomEvent) { + const target = ev.target as any; + let value = Number(target.value); + + if (isNaN(value)) { + return; + } + + const oldValue = this._colorBrightnessSliderValue; + this._colorBrightnessSliderValue = value; + + value = (value * 255) / 100; + + const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [ + 255, 255, 255, + ]) as [number, number, number]; + + this._setRgbWColor( + this._adjustColorBrightness( + // first normalize the value + oldValue + ? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true) + : rgb, + value + ) + ); + } + + private _segmentClick() { + if (this._hueSegments === 24 && this._saturationSegments === 8) { + this._hueSegments = 0; + this._saturationSegments = 0; + } else { + this._hueSegments = 24; + this._saturationSegments = 8; + } + } + + private _adjustColorBrightness( + rgbColor: [number, number, number], + value?: number, + invert = false + ) { + if (value !== undefined && value !== 255) { + let ratio = value / 255; + if (invert) { + ratio = 1 / ratio; + } + rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio)); + rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio)); + rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio)); + } + return rgbColor; + } + + private _setRgbWColor(rgbColor: [number, number, number]) { + if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { + const rgbww_color: [number, number, number, number, number] = this + .stateObj!.attributes.rgbww_color + ? [...this.stateObj!.attributes.rgbww_color] + : [0, 0, 0, 0, 0]; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbww_color: rgbColor.concat(rgbww_color.slice(3)), + }); + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { + const rgbw_color: [number, number, number, number] = this.stateObj! + .attributes.rgbw_color + ? [...this.stateObj!.attributes.rgbw_color] + : [0, 0, 0, 0]; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbw_color: rgbColor.concat(rgbw_color.slice(3)), + }); + } + } + + /** + * Called when a new color has been picked. + * should be throttled with the 'throttle=' attribute of the color picker + */ + private _colorPicked( + ev: CustomEvent<{ + hs: { h: number; s: number }; + rgb: { r: number; g: number; b: number }; + }> + ) { + this._colorPickerColor = [ + ev.detail.rgb.r, + ev.detail.rgb.g, + ev.detail.rgb.b, + ]; + + if ( + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) + ) { + this._setRgbWColor( + this._colorBrightnessSliderValue + ? this._adjustColorBrightness( + [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b], + (this._colorBrightnessSliderValue * 255) / 100 + ) + : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] + ); + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { + const rgb_color: [number, number, number] = [ + ev.detail.rgb.r, + ev.detail.rgb.g, + ev.detail.rgb.b, + ]; + if (this._brightnessAdjusted) { + const brightnessAdjust = (this._brightnessAdjusted / 255) * 100; + const brightnessPercentage = Math.round( + ((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255 + ); + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + brightness_pct: brightnessPercentage, + rgb_color: this._adjustColorBrightness( + rgb_color, + this._brightnessAdjusted, + true + ), + }); + } else { + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgb_color, + }); + } + } else { + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], + }); + } + } + + private _close(): void { + this._params = undefined; + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0px; + } + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + } + .content { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + } + + .content > * { + width: 100%; + } + + .segmentationContainer { + position: relative; + max-height: 500px; + display: flex; + justify-content: center; + } + + .segmentationButton { + position: absolute; + top: 5%; + left: 0; + color: var(--secondary-text-color); + } + + ha-color-picker { + --ha-color-picker-wheel-borderwidth: 5; + --ha-color-picker-wheel-bordercolor: white; + --ha-color-picker-wheel-shadow: none; + --ha-color-picker-marker-borderwidth: 2; + --ha-color-picker-marker-bordercolor: white; + } + + ha-more-info-bar-slider { + --more-info-slider-bar-height: 290px; + margin: 20px 0; + } + + .color_temp { + --more-info-slider-bar-background: -webkit-linear-gradient( + top, + rgb(166, 209, 255) 0%, + white 50%, + rgb(255, 160, 0) 100% + ); + --more-info-slider-bar-background-opacity: 1; + } + + hr { + border-color: var(--divider-color); + border-bottom: none; + margin: 16px 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-light-color-picker": DialogLightColorPicker; + } +} diff --git a/src/dialogs/more-info/components/lights/show-dialog-light-color-picker.ts b/src/dialogs/more-info/components/lights/show-dialog-light-color-picker.ts new file mode 100644 index 0000000000..41d82429cb --- /dev/null +++ b/src/dialogs/more-info/components/lights/show-dialog-light-color-picker.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface LightColorPickerDialogParams { + entityId: string; +} + +export const loadLightColorPickerDialog = () => + import("./dialog-light-color-picker"); + +export const showLightColorPickerDialog = ( + element: HTMLElement, + dialogParams: LightColorPickerDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-light-color-picker", + dialogImport: loadLightColorPickerDialog, + dialogParams, + }); +}; diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 7ce94dabbf..32eca23af9 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,34 +1,24 @@ import "@material/mwc-list/mwc-list-item"; -import { mdiPalette } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; +import "@material/web/iconbutton/outlined-icon-button"; +import { mdiPalette, mdiShimmer } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; -import "../../../components/ha-button-toggle-group"; -import "../../../components/ha-color-picker"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-labeled-slider"; +import "../../../components/ha-button-menu"; import "../../../components/ha-select"; import { - getLightCurrentModeRgbColor, LightColorMode, LightEntity, LightEntityFeature, - lightIsInColorMode, + lightSupportsBrightness, lightSupportsColor, lightSupportsColorMode, - lightSupportsBrightness, } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; +import "../components/ha-more-info-light-brightness"; +import { showLightColorPickerDialog } from "../components/lights/show-dialog-light-color-picker"; @customElement("more-info-light") class MoreInfoLight extends LitElement { @@ -36,198 +26,90 @@ class MoreInfoLight extends LitElement { @property({ attribute: false }) public stateObj?: LightEntity; - @state() private _brightnessSliderValue = 0; - - @state() private _ctSliderValue?: number; - - @state() private _cwSliderValue?: number; - - @state() private _wwSliderValue?: number; - - @state() private _wvSliderValue?: number; - - @state() private _colorBrightnessSliderValue?: number; - - @state() private _brightnessAdjusted?: number; - - @state() private _hueSegments = 24; - - @state() private _saturationSegments = 8; - - @state() private _colorPickerColor?: [number, number, number]; - - @state() private _mode?: "color" | LightColorMode; - protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } - const supportsTemp = lightSupportsColorMode( + const supportsColorTemp = lightSupportsColorMode( this.stateObj, LightColorMode.COLOR_TEMP ); - const supportsWhite = lightSupportsColorMode( + const supportsColor = lightSupportsColor(this.stateObj); + + const supportsBrightness = lightSupportsBrightness(this.stateObj); + + const supportsEffects = supportsFeature( this.stateObj, - LightColorMode.WHITE + LightEntityFeature.EFFECT ); - const supportsRgbww = lightSupportsColorMode( - this.stateObj, - LightColorMode.RGBWW - ); - - const supportsRgbw = - !supportsRgbww && - lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); - - const supportsColor = - supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); - return html`
- ${lightSupportsBrightness(this.stateObj) + ${supportsBrightness ? html` - + + ` : ""} - ${this.stateObj.state === "on" + ${supportsColorTemp || supportsColor || supportsEffects ? html` - ${supportsTemp || supportsColor ? html`
` : ""} - ${supportsColor && (supportsTemp || supportsWhite) - ? html`` - : ""} - ${supportsTemp && - ((!supportsColor && !supportsWhite) || - this._mode === LightColorMode.COLOR_TEMP) - ? html` - - ` - : ""} - ${supportsColor && - ((!supportsTemp && !supportsWhite) || this._mode === "color") - ? html` -
- + ${supportsColorTemp || supportsColor + ? html` + - - -
- - ${supportsRgbw || supportsRgbww - ? html` + + ` + : null} + ${supportsEffects + ? html` + + ` - : ""} - ${supportsRgbw - ? html` - - ` - : ""} - ${supportsRgbww - ? html` - - - ` - : ""} - ` - : ""} - ${supportsFeature(this.stateObj, LightEntityFeature.EFFECT) && - this.stateObj!.attributes.effect_list?.length - ? html` -
- - ${this.stateObj.attributes.effect_list.map( - (effect: string) => html` - - ${effect} - - ` - )} - - ` - : ""} + .ariaLabel=${this.hass.localize( + "ui.dialogs.more_info_control.light.select_effect" + )} + > + + + ${this.stateObj.attributes.effect_list!.map( + (effect: string) => html` + + ${effect} + + ` + )} + + ` + : null} +
` - : ""} + : null} ) { - super.willUpdate(changedProps); + private _showLightColorPickerDialog = () => { + showLightColorPickerDialog(this, { entityId: this.stateObj!.entity_id }); + }; - if (!changedProps.has("stateObj")) { - return; - } - const stateObj = this.stateObj! as LightEntity; - const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined; + private _handleEffectButton(ev) { + ev.stopPropagation(); + ev.preventDefault(); - if (stateObj.state === "on") { - // Don't change tab when the color mode changes - if ( - oldStateObj?.entity_id !== stateObj.entity_id || - oldStateObj?.state !== stateObj.state - ) { - this._mode = lightIsInColorMode(this.stateObj!) - ? "color" - : this.stateObj!.attributes.color_mode; - } + const index = ev.detail.index; + const effect = this.stateObj!.attributes.effect_list![index]; - let brightnessAdjust = 100; - this._brightnessAdjusted = undefined; - if ( - stateObj.attributes.color_mode === LightColorMode.RGB && - !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && - !lightSupportsColorMode(stateObj, LightColorMode.RGBW) - ) { - const maxVal = Math.max(...stateObj.attributes.rgb_color!); - if (maxVal < 255) { - this._brightnessAdjusted = maxVal; - brightnessAdjust = (this._brightnessAdjusted / 255) * 100; - } - } - this._brightnessSliderValue = Math.round( - ((stateObj.attributes.brightness || 0) * brightnessAdjust) / 255 - ); - this._ctSliderValue = stateObj.attributes.color_temp_kelvin; - this._wvSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBW - ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) - : undefined; - this._cwSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBWW - ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) - : undefined; - this._wwSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBWW - ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) - : undefined; - - const currentRgbColor = getLightCurrentModeRgbColor(stateObj); - - this._colorBrightnessSliderValue = currentRgbColor - ? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255) - : undefined; - - this._colorPickerColor = currentRgbColor?.slice(0, 3) as [ - number, - number, - number - ]; - } else { - this._brightnessSliderValue = 0; - } - } - - private _toggleButtons = memoizeOne( - (supportsTemp: boolean, supportsWhite: boolean) => { - const modes = [{ label: "Color", value: "color" }]; - if (supportsTemp) { - modes.push({ label: "Temperature", value: LightColorMode.COLOR_TEMP }); - } - if (supportsWhite) { - modes.push({ label: "White", value: LightColorMode.WHITE }); - } - return modes; - } - ); - - private _modeChanged(ev: CustomEvent) { - this._mode = ev.detail.value; - } - - private _effectChanged(ev) { - const newVal = ev.target.value; - - if (!newVal || this.stateObj!.attributes.effect === newVal) { + if (!effect || this.stateObj!.attributes.effect === effect) { return; } this.hass.callService("light", "turn_on", { entity_id: this.stateObj!.entity_id, - effect: newVal, + effect, }); } - private _brightnessSliderChanged(ev: CustomEvent) { - const bri = Number((ev.target as any).value); - - if (isNaN(bri)) { - return; - } - - this._brightnessSliderValue = bri; - - if (this._mode === LightColorMode.WHITE) { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - white: Math.min(255, Math.round((bri * 255) / 100)), - }); - return; - } - - if (this._brightnessAdjusted) { - const rgb = - this.stateObj!.attributes.rgb_color || - ([0, 0, 0] as [number, number, number]); - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: bri, - rgb_color: this._adjustColorBrightness( - rgb, - this._brightnessAdjusted, - true - ), - }); - return; - } - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: bri, - }); - } - - private _ctSliderChanged(ev: CustomEvent) { - const ct = Number((ev.target as any).value); - - if (isNaN(ct)) { - return; - } - - this._ctSliderValue = ct; - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - color_temp_kelvin: ct, - }); - } - - private _wvSliderChanged(ev: CustomEvent) { - const target = ev.target as any; - let wv = Number(target.value); - const name = target.name; - - if (isNaN(wv)) { - return; - } - - if (name === "wv") { - this._wvSliderValue = wv; - } else if (name === "cw") { - this._cwSliderValue = wv; - } else if (name === "ww") { - this._wwSliderValue = wv; - } - - wv = Math.min(255, Math.round((wv * 255) / 100)); - - const rgb = getLightCurrentModeRgbColor(this.stateObj!); - - if (name === "wv") { - const rgbw_color = rgb || [0, 0, 0, 0]; - rgbw_color[3] = wv; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbw_color, - }); - return; - } - - const rgbww_color = rgb || [0, 0, 0, 0, 0]; - while (rgbww_color.length < 5) { - rgbww_color.push(0); - } - rgbww_color[name === "cw" ? 3 : 4] = wv; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbww_color, - }); - } - - private _colorBrightnessSliderChanged(ev: CustomEvent) { - const target = ev.target as any; - let value = Number(target.value); - - if (isNaN(value)) { - return; - } - - const oldValue = this._colorBrightnessSliderValue; - this._colorBrightnessSliderValue = value; - - value = (value * 255) / 100; - - const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [ - 255, 255, 255, - ]) as [number, number, number]; - - this._setRgbWColor( - this._adjustColorBrightness( - // first normalize the value - oldValue - ? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true) - : rgb, - value - ) - ); - } - - private _segmentClick() { - if (this._hueSegments === 24 && this._saturationSegments === 8) { - this._hueSegments = 0; - this._saturationSegments = 0; - } else { - this._hueSegments = 24; - this._saturationSegments = 8; - } - } - - private _adjustColorBrightness( - rgbColor: [number, number, number], - value?: number, - invert = false - ) { - if (value !== undefined && value !== 255) { - let ratio = value / 255; - if (invert) { - ratio = 1 / ratio; - } - rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio)); - rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio)); - rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio)); - } - return rgbColor; - } - - private _setRgbWColor(rgbColor: [number, number, number]) { - if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { - const rgbww_color: [number, number, number, number, number] = this - .stateObj!.attributes.rgbww_color - ? [...this.stateObj!.attributes.rgbww_color] - : [0, 0, 0, 0, 0]; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbww_color: rgbColor.concat(rgbww_color.slice(3)), - }); - } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { - const rgbw_color: [number, number, number, number] = this.stateObj! - .attributes.rgbw_color - ? [...this.stateObj!.attributes.rgbw_color] - : [0, 0, 0, 0]; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbw_color: rgbColor.concat(rgbw_color.slice(3)), - }); - } - } - - /** - * Called when a new color has been picked. - * should be throttled with the 'throttle=' attribute of the color picker - */ - private _colorPicked( - ev: CustomEvent<{ - hs: { h: number; s: number }; - rgb: { r: number; g: number; b: number }; - }> - ) { - this._colorPickerColor = [ - ev.detail.rgb.r, - ev.detail.rgb.g, - ev.detail.rgb.b, - ]; - - if ( - lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || - lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) - ) { - this._setRgbWColor( - this._colorBrightnessSliderValue - ? this._adjustColorBrightness( - [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b], - (this._colorBrightnessSliderValue * 255) / 100 - ) - : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] - ); - } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { - const rgb_color: [number, number, number] = [ - ev.detail.rgb.r, - ev.detail.rgb.g, - ev.detail.rgb.b, - ]; - if (this._brightnessAdjusted) { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: this._brightnessSliderValue, - rgb_color: this._adjustColorBrightness( - rgb_color, - this._brightnessAdjusted, - true - ), - }); - } else { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgb_color, - }); - } - } else { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], - }); - } - } - static get styles(): CSSResultGroup { return css` .content { @@ -577,48 +152,18 @@ class MoreInfoLight extends LitElement { width: 100%; } - .color_temp { - --ha-slider-background: -webkit-linear-gradient( - var(--float-end), - rgb(166, 209, 255) 0%, - white 50%, - rgb(255, 160, 0) 100% - ); - /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ - --paper-slider-knob-start-border-color: var(--primary-color); - margin-bottom: 4px; - } - - .segmentationContainer { - position: relative; - max-height: 500px; + .buttons { display: flex; + align-items: center; justify-content: center; + margin-bottom: 12px; + } + .buttons > * { + margin: 4px; } - ha-button-toggle-group { - margin-bottom: 8px; - } - - ha-color-picker { - --ha-color-picker-wheel-borderwidth: 5; - --ha-color-picker-wheel-bordercolor: white; - --ha-color-picker-wheel-shadow: none; - --ha-color-picker-marker-borderwidth: 2; - --ha-color-picker-marker-bordercolor: white; - } - - .segmentationButton { - position: absolute; - top: 5%; - left: 0; - color: var(--secondary-text-color); - } - - hr { - border-color: var(--divider-color); - border-bottom: none; - margin: 16px 0; + ha-more-info-light-brightness { + margin-bottom: 16px; } `; } diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index d4ebaed326..ad5d0b64e0 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -1,9 +1,9 @@ import type { HassEntity } from "home-assistant-js-websocket"; -import { - DOMAINS_WITH_MORE_INFO, - DOMAINS_HIDE_DEFAULT_MORE_INFO, -} from "./const"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { + DOMAINS_HIDE_DEFAULT_MORE_INFO, + DOMAINS_WITH_MORE_INFO, +} from "./const"; const LAZY_LOADED_MORE_INFO_CONTROL = { alarm_control_panel: () => import("./controls/more-info-alarm_control_panel"), @@ -33,7 +33,6 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { export const stateMoreInfoType = (stateObj: HassEntity): string => { const domain = computeStateDomain(stateObj); - return domainMoreInfoType(domain); }; diff --git a/src/translations/en.json b/src/translations/en.json index 8821a06204..f5631ef552 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -889,6 +889,18 @@ }, "zone": { "graph_unit": "People home" + }, + "light": { + "change_color": "Change color", + "select_effect": "Select effect", + "color_picker": { + "title": "Change color", + "mode": { + "color": "Color", + "color_temp": "Temperature", + "white": "White" + } + } } }, "entity_registry": { diff --git a/yarn.lock b/yarn.lock index 621c90a886..f4446890ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3117,6 +3117,16 @@ __metadata: languageName: node linkType: hard +"@material/web@npm:^1.0.0-pre.1": + version: 1.0.0-pre.1 + resolution: "@material/web@npm:1.0.0-pre.1" + dependencies: + lit: ^2.3.0 + tslib: ^2.4.0 + checksum: 87716cb760020380f797feebb066a6e22ecd9d97b2fdb697977ca9af2b58b7422737a38cb0639cdb1124669d650ce3cfe1e77642738ef36556116ced57d82a1f + languageName: node + linkType: hard + "@mdi/js@npm:7.1.96": version: 7.1.96 resolution: "@mdi/js@npm:7.1.96" @@ -9493,6 +9503,7 @@ fsevents@^1.2.7: "@material/mwc-textfield": ^0.27.0 "@material/mwc-top-app-bar-fixed": ^0.27.0 "@material/top-app-bar": =14.0.0-canary.53b3cad2f.0 + "@material/web": ^1.0.0-pre.1 "@mdi/js": 7.1.96 "@mdi/svg": 7.1.96 "@octokit/auth-oauth-device": ^4.0.2 @@ -11214,7 +11225,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"lit@npm:^2.0.0, lit@npm:^2.0.0-rc.2, lit@npm:^2.2.1, lit@npm:^2.5.0, lit@npm:^2.6.1": +"lit@npm:^2.0.0, lit@npm:^2.0.0-rc.2, lit@npm:^2.2.1, lit@npm:^2.3.0, lit@npm:^2.5.0, lit@npm:^2.6.1": version: 2.6.1 resolution: "lit@npm:2.6.1" dependencies: