From 36586b798ee5e3726112a8009d2f5e618cfe9ffb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 Apr 2021 19:43:25 +0200 Subject: [PATCH] Use `supported_color_modes` to determine what UI elements to show (#8961) --- gallery/src/demos/demo-more-info-light.ts | 106 ++++- src/common/color/convert-color.ts | 15 + src/components/entity/state-badge.ts | 13 +- src/components/ha-button-toggle-group.ts | 8 + src/components/ha-color-picker.js | 46 +- src/data/light.ts | 76 +++- .../more-info/controls/more-info-light.ts | 401 +++++++++++++++--- src/panels/lovelace/cards/hui-button-card.ts | 14 +- src/panels/lovelace/cards/hui-light-card.ts | 23 +- src/translations/en.json | 5 +- 10 files changed, 587 insertions(+), 120 deletions(-) diff --git a/gallery/src/demos/demo-more-info-light.ts b/gallery/src/demos/demo-more-info-light.ts index d469da2212..399873aa9c 100644 --- a/gallery/src/demos/demo-more-info-light.ts +++ b/gallery/src/demos/demo-more-info-light.ts @@ -9,13 +9,10 @@ import { } from "lit-element"; import "../../../src/components/ha-card"; import { - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + LightColorModes, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, } from "../../../src/data/light"; import "../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../src/fake_data/entity"; @@ -32,7 +29,8 @@ const ENTITIES = [ getEntity("light", "kitchen_light", "on", { friendly_name: "Brightness Light", brightness: 200, - supported_features: SUPPORT_BRIGHTNESS, + supported_color_modes: [LightColorModes.BRIGHTNESS], + color_mode: LightColorModes.BRIGHTNESS, }), getEntity("light", "color_temperature_light", "on", { friendly_name: "White Color Temperature Light", @@ -40,20 +38,96 @@ const ENTITIES = [ color_temp: 75, min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_BRIGHTNESS + SUPPORT_COLOR_TEMP, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + ], + color_mode: LightColorModes.COLOR_TEMP, }), - getEntity("light", "color_effectslight", "on", { - friendly_name: "Color Effets Light", + getEntity("light", "color_hs_light", "on", { + friendly_name: "Color HS Light", brightness: 255, hs_color: [30, 100], - white_value: 36, - supported_features: - SUPPORT_BRIGHTNESS + - SUPPORT_EFFECT + - SUPPORT_FLASH + - SUPPORT_COLOR + - SUPPORT_TRANSITION + - SUPPORT_WHITE_VALUE, + rgb_color: [30, 100, 255], + min_mireds: 30, + max_mireds: 150, + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + LightColorModes.HS, + ], + color_mode: LightColorModes.HS, + effect_list: ["random", "colorloop"], + }), + getEntity("light", "color_rgb_ct_light", "on", { + friendly_name: "Color RGB + CT Light", + brightness: 255, + color_temp: 75, + min_mireds: 30, + max_mireds: 150, + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + LightColorModes.RGB, + ], + color_mode: LightColorModes.COLOR_TEMP, + effect_list: ["random", "colorloop"], + }), + getEntity("light", "color_RGB_light", "on", { + friendly_name: "Color Effets Light", + brightness: 255, + rgb_color: [30, 100, 255], + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [LightColorModes.BRIGHTNESS, LightColorModes.RGB], + color_mode: LightColorModes.RGB, + effect_list: ["random", "colorloop"], + }), + getEntity("light", "color_rgbw_light", "on", { + friendly_name: "Color RGBW Light", + brightness: 255, + rgbw_color: [30, 100, 255, 125], + min_mireds: 30, + max_mireds: 150, + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + LightColorModes.RGBW, + ], + color_mode: LightColorModes.RGBW, + effect_list: ["random", "colorloop"], + }), + getEntity("light", "color_rgbww_light", "on", { + friendly_name: "Color RGBWW Light", + brightness: 255, + rgbww_color: [30, 100, 255, 125, 10], + min_mireds: 30, + max_mireds: 150, + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + LightColorModes.RGBWW, + ], + color_mode: LightColorModes.RGBWW, + effect_list: ["random", "colorloop"], + }), + getEntity("light", "color_xy_light", "on", { + friendly_name: "Color XY Light", + brightness: 255, + xy_color: [30, 100], + rgb_color: [30, 100, 255], + min_mireds: 30, + max_mireds: 150, + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_color_modes: [ + LightColorModes.BRIGHTNESS, + LightColorModes.COLOR_TEMP, + LightColorModes.XY, + ], + color_mode: LightColorModes.XY, effect_list: ["random", "colorloop"], }), ]; diff --git a/src/common/color/convert-color.ts b/src/common/color/convert-color.ts index 29f95b4283..a0716a68e4 100644 --- a/src/common/color/convert-color.ts +++ b/src/common/color/convert-color.ts @@ -102,3 +102,18 @@ export const lab2hex = (lab: [number, number, number]): string => { const rgb = lab2rgb(lab); return rgb2hex(rgb); }; + +export const rgb2hsv = ( + rgb: [number, number, number] +): [number, number, number] => { + const [r, g, b] = rgb; + const v = Math.max(r, g, b); + const c = v - Math.min(r, g, b); + const h = + c && (v === r ? (g - b) / c : v === g ? 2 + (b - r) / c : 4 + (r - g) / c); + return [60 * (h < 0 ? h + 6 : h), v && c / v, v]; +}; + +export const rgb2hs = (rgb: [number, number, number]): [number, number] => { + return rgb2hsv(rgb).slice(0, 2) as [number, number]; +}; diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index 0b67adcc6a..2fa735f5b6 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -15,6 +15,7 @@ import { computeActiveState } from "../../common/entity/compute_active_state"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { stateIcon } from "../../common/entity/state_icon"; import { iconColorCSS } from "../../common/style/icon_color_css"; +import { getLightRgbColor, LightEntity } from "../../data/light"; import type { HomeAssistant } from "../../types"; import "../ha-icon"; @@ -99,11 +100,13 @@ export class StateBadge extends LitElement { hostStyle.backgroundImage = `url(${imageUrl})`; this._showIcon = false; } else if (stateObj.state === "on") { - if (stateObj.attributes.hs_color && this.stateColor !== false) { - const hue = stateObj.attributes.hs_color[0]; - const sat = stateObj.attributes.hs_color[1]; - if (sat > 10) { - iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`; + if ( + computeStateDomain(stateObj) === "light" && + this.stateColor !== false + ) { + const rgb = getLightRgbColor(stateObj as LightEntity); + if (rgb) { + iconStyle.color = `rgb(${rgb.slice(0, 3).join(",")})`; } } if (stateObj.attributes.brightness && this.stateColor !== false) { diff --git a/src/components/ha-button-toggle-group.ts b/src/components/ha-button-toggle-group.ts index 33c65cdf82..8facec61bf 100644 --- a/src/components/ha-button-toggle-group.ts +++ b/src/components/ha-button-toggle-group.ts @@ -9,6 +9,7 @@ import { property, TemplateResult, } from "lit-element"; +import { styleMap } from "lit-html/directives/style-map"; import { fireEvent } from "../common/dom/fire_event"; import type { ToggleButton } from "../types"; import "./ha-svg-icon"; @@ -19,6 +20,8 @@ export class HaButtonToggleGroup extends LitElement { @property() public active?: string; + @property({ type: Boolean }) public fullWidth = false; + protected render(): TemplateResult { return html`
@@ -33,6 +36,11 @@ export class HaButtonToggleGroup extends LitElement { ` : html` @@ -282,12 +291,13 @@ class HaColorPicker extends EventsMixin(PolymerElement) { processUserSelect(ev) { const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); const hs = this.getColor(canvasXY.x, canvasXY.y); - this.onColorSelect(hs); + const rgb = this.getRgbColor(canvasXY.x, canvasXY.y); + this.onColorSelect(hs, rgb); } // apply color to marker position and canvas - onColorSelect(hs) { - this.setMarkerOnColor(hs); // marker always follows mounse 'raw' hs value (= mouse position) + onColorSelect(hs, rgb) { + this.setMarkerOnColor(hs); // marker always follows mouse 'raw' hs value (= mouse position) if (!this.ignoreSegments) { // apply segments if needed hs = this.applySegmentFilter(hs); @@ -301,11 +311,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) { // eventually after throttle limit has passed clearTimeout(this.ensureFinalSelect); this.ensureFinalSelect = setTimeout(() => { - this.fireColorSelected(hs); // do it for the final time + this.fireColorSelected(hs, rgb); // do it for the final time }, this.throttle); return; } - this.fireColorSelected(hs); // do it + this.fireColorSelected(hs, rgb); // do it this.colorSelectIsThrottled = true; setTimeout(() => { this.colorSelectIsThrottled = false; @@ -313,9 +323,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) { } // set color values and fire colorselected event - fireColorSelected(hs) { + fireColorSelected(hs, rgb) { this.hsColor = hs; - this.fire("colorselected", { hs: { h: hs.h, s: hs.s } }); + this.fire("colorselected", { hs, rgb }); } /* @@ -363,6 +373,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) { this.applyColorToCanvas(hs); } + applyRgbColor(rgb) { + const [h, s] = rgb2hs(rgb); + this.applyHsColor({ h, s }); + } + /* * input processing helpers */ @@ -395,6 +410,15 @@ class HaColorPicker extends EventsMixin(PolymerElement) { return { h: hue, s: sat }; } + getRgbColor(x, y) { + // get current pixel + const imageData = this.backgroundLayer + .getContext("2d") + .getImageData(x + 250, y + 250, 1, 1); + const pixel = imageData.data; + return { r: pixel[0], g: pixel[1], b: pixel[2] }; + } + applySegmentFilter(hs) { // apply hue segment steps if (this.hueSegments) { @@ -468,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) { .getPropertyValue("--wheel-bordercolor") .trim(); const wheelShadow = wheelStyle.getPropertyValue("--wheel-shadow").trim(); - // extract shadow properties from CCS variable + // extract shadow properties from CSS variable // the shadow should be defined as: "10px 5px 5px 0px COLOR" if (wheelShadow !== "none") { const values = wheelShadow.split("px "); diff --git a/src/data/light.ts b/src/data/light.ts index ff0070e7d0..27f335709e 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -3,26 +3,82 @@ import { HassEntityBase, } from "home-assistant-js-websocket"; +export enum LightColorModes { + UNKNOWN = "unknown", + ONOFF = "onoff", + BRIGHTNESS = "brightness", + COLOR_TEMP = "color_temp", + HS = "hs", + XY = "xy", + RGB = "rgb", + RGBW = "rgbw", + RGBWW = "rgbww", +} + +const modesSupportingColor = [ + LightColorModes.HS, + LightColorModes.XY, + LightColorModes.RGB, + LightColorModes.RGBW, + LightColorModes.RGBWW, +]; + +const modesSupportingDimming = [ + ...modesSupportingColor, + LightColorModes.COLOR_TEMP, + LightColorModes.BRIGHTNESS, +]; + +export const SUPPORT_EFFECT = 4; +export const SUPPORT_FLASH = 8; +export const SUPPORT_TRANSITION = 32; + +export const lightSupportsColorMode = ( + entity: LightEntity, + mode: LightColorModes +) => { + return entity.attributes.supported_color_modes?.includes(mode); +}; + +export const lightIsInColorMode = (entity: LightEntity) => { + return modesSupportingColor.includes(entity.attributes.color_mode); +}; + +export const lightSupportsColor = (entity: LightEntity) => { + return entity.attributes.supported_color_modes?.some((mode) => + modesSupportingColor.includes(mode) + ); +}; + +export const lightSupportsDimming = (entity: LightEntity) => { + return entity.attributes.supported_color_modes?.some((mode) => + modesSupportingDimming.includes(mode) + ); +}; + +export const getLightRgbColor = (entity: LightEntity): number[] | undefined => + entity.attributes.color_mode === LightColorModes.RGBWW + ? entity.attributes.rgbww_color + : entity.attributes.color_mode === LightColorModes.RGBW + ? entity.attributes.rgbw_color + : entity.attributes.rgb_color; + interface LightEntityAttributes extends HassEntityAttributeBase { min_mireds: number; max_mireds: number; friendly_name: string; brightness: number; - hs_color: number[]; + hs_color: [number, number]; + rgb_color: [number, number, number]; + rgbw_color: [number, number, number, number]; + rgbww_color: [number, number, number, number, number]; color_temp: number; - white_value: number; effect?: string; effect_list: string[] | null; + supported_color_modes: LightColorModes[]; + color_mode: LightColorModes; } export interface LightEntity extends HassEntityBase { attributes: LightEntityAttributes; } - -export const SUPPORT_BRIGHTNESS = 1; -export const SUPPORT_COLOR_TEMP = 2; -export const SUPPORT_EFFECT = 4; -export const SUPPORT_FLASH = 8; -export const SUPPORT_COLOR = 16; -export const SUPPORT_TRANSITION = 32; -export const SUPPORT_WHITE_VALUE = 128; diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index b0f686e412..7bb67a8b77 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -11,7 +11,6 @@ import { PropertyValues, TemplateResult, } from "lit-element"; -import { classMap } from "lit-html/directives/class-map"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; import "../../../components/ha-color-picker"; @@ -19,20 +18,22 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-labeled-slider"; import "../../../components/ha-paper-dropdown-menu"; import { + getLightRgbColor, + LightColorModes, LightEntity, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + lightIsInColorMode, + lightSupportsColor, + lightSupportsColorMode, + lightSupportsDimming, SUPPORT_EFFECT, - SUPPORT_WHITE_VALUE, } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; +import "../../../components/ha-button-toggle-group"; -interface HueSatColor { - h: number; - s: number; -} - +const toggleButtons = [ + { label: "Color", value: "color" }, + { label: "Temperature", value: LightColorModes.COLOR_TEMP }, +]; @customElement("more-info-light") class MoreInfoLight extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -41,28 +42,51 @@ class MoreInfoLight extends LitElement { @internalProperty() private _brightnessSliderValue = 0; - @internalProperty() private _ctSliderValue = 0; + @internalProperty() private _ctSliderValue?: number; - @internalProperty() private _wvSliderValue = 0; + @internalProperty() private _cwSliderValue?: number; + + @internalProperty() private _wwSliderValue?: number; + + @internalProperty() private _wvSliderValue?: number; + + @internalProperty() private _colorBrightnessSliderValue?: number; + + @internalProperty() private _brightnessAdjusted?: number; @internalProperty() private _hueSegments = 24; @internalProperty() private _saturationSegments = 8; - @internalProperty() private _colorPickerColor?: HueSatColor; + @internalProperty() private _colorPickerColor?: [number, number, number]; + + @internalProperty() private _mode?: "color" | LightColorModes.COLOR_TEMP; protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } + const supportsTemp = lightSupportsColorMode( + this.stateObj, + LightColorModes.COLOR_TEMP + ); + + const supportsRgbww = lightSupportsColorMode( + this.stateObj, + LightColorModes.RGBWW + ); + + const supportsRgbw = + !supportsRgbww && + lightSupportsColorMode(this.stateObj, LightColorModes.RGBW); + + const supportsColor = + supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); + return html` -
- ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS) +
+ ${lightSupportsDimming(this.stateObj) ? html` ` : ""} + ${supportsTemp && supportsColor + ? html`` + : ""} + ${supportsTemp && + (!supportsColor || this._mode === LightColorModes.COLOR_TEMP) ? html` +
` : ""} - ${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE) - ? html` - - ` - : ""} - ${supportsFeature(this.stateObj, SUPPORT_COLOR) + ${supportsColor && (!supportsTemp || this._mode === "color") ? html`
+ + ${ + supportsRgbw || supportsRgbww + ? html`` + : "" + } + ${ + supportsRgbw + ? html` + + ` + : "" + } + ${ + supportsRgbww + ? html` + + + ` + : "" + } +
` : ""} ${supportsFeature(this.stateObj, SUPPORT_EFFECT) && @@ -151,34 +235,85 @@ class MoreInfoLight extends LitElement { : ""}
`; } - protected updated(changedProps: PropertyValues): void { + protected updated(changedProps: PropertyValues) { + if (!changedProps.has("stateObj")) { + return; + } const stateObj = this.stateObj! as LightEntity; - if (changedProps.has("stateObj")) { - if (stateObj.state === "on") { - this._brightnessSliderValue = Math.round( - (stateObj.attributes.brightness * 100) / 255 - ); - this._ctSliderValue = stateObj.attributes.color_temp; - this._wvSliderValue = stateObj.attributes.white_value; + const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined; - if (stateObj.attributes.hs_color) { - this._colorPickerColor = { - h: stateObj.attributes.hs_color[0], - s: stateObj.attributes.hs_color[1] / 100, - }; + 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" + : LightColorModes.COLOR_TEMP; + } + + let brightnessAdjust = 100; + if ( + stateObj.attributes.color_mode === LightColorModes.RGB && + !lightSupportsColorMode(stateObj, LightColorModes.RGBWW) && + !lightSupportsColorMode(stateObj, LightColorModes.RGBW) + ) { + const maxVal = Math.max(...stateObj.attributes.rgb_color); + if (maxVal < 255) { + this._brightnessAdjusted = maxVal; + brightnessAdjust = (this._brightnessAdjusted / 255) * 100; } } else { - this._brightnessSliderValue = 0; + this._brightnessAdjusted = undefined; } + this._brightnessSliderValue = Math.round( + (stateObj.attributes.brightness * brightnessAdjust) / 255 + ); + this._ctSliderValue = stateObj.attributes.color_temp; + this._wvSliderValue = + stateObj.attributes.color_mode === LightColorModes.RGBW + ? Math.round((stateObj.attributes.rgbw_color[3] * 100) / 255) + : undefined; + this._cwSliderValue = + stateObj.attributes.color_mode === LightColorModes.RGBWW + ? Math.round((stateObj.attributes.rgbww_color[3] * 100) / 255) + : undefined; + this._wwSliderValue = + stateObj.attributes.color_mode === LightColorModes.RGBWW + ? Math.round((stateObj.attributes.rgbww_color[4] * 100) / 255) + : undefined; + this._colorBrightnessSliderValue = + stateObj.attributes.color_mode === LightColorModes.RGBWW + ? Math.round( + (Math.max(...stateObj.attributes.rgbww_color.slice(0, 3)) * 100) / + 255 + ) + : stateObj.attributes.color_mode === LightColorModes.RGBW + ? Math.round( + (Math.max(...stateObj.attributes.rgbw_color.slice(0, 3)) * 100) / + 255 + ) + : undefined; + + this._colorPickerColor = getLightRgbColor(stateObj)?.slice(0, 3) as + | [number, number, number] + | undefined; + } else { + this._brightnessSliderValue = 0; } } + private _modeChanged(ev: CustomEvent) { + this._mode = ev.detail.value; + } + private _effectChanged(ev: CustomEvent) { const newVal = ev.detail.item.itemName; @@ -193,12 +328,29 @@ class MoreInfoLight extends LitElement { } private _brightnessSliderChanged(ev: CustomEvent) { - const bri = parseInt((ev.target as any).value, 10); + const bri = Number((ev.target as any).value); if (isNaN(bri)) { 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, @@ -206,7 +358,7 @@ class MoreInfoLight extends LitElement { } private _ctSliderChanged(ev: CustomEvent) { - const ct = parseInt((ev.target as any).value, 10); + const ct = Number((ev.target as any).value); if (isNaN(ct)) { return; @@ -219,18 +371,64 @@ class MoreInfoLight extends LitElement { } private _wvSliderChanged(ev: CustomEvent) { - const wv = parseInt((ev.target as any).value, 10); + const target = ev.target as any; + let wv = Number(target.value); + const name = target.name; if (isNaN(wv)) { return; } + wv = (wv * 255) / 100; + + const rgb = getLightRgbColor(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, - white_value: wv, + rgbww_color, }); } + private _colorBrightnessSliderChanged(ev: CustomEvent) { + const target = ev.target as any; + const value = Number(target.value); + + const rgb = (getLightRgbColor(this.stateObj!)?.slice(0, 3) || [ + 255, + 255, + 255, + ]) as [number, number, number]; + + this._setRgbColor( + this._adjustColorBrightness( + // first normalize the value + this._colorBrightnessSliderValue + ? this._adjustColorBrightness( + rgb, + this._colorBrightnessSliderValue, + true + ) + : rgb, + value + ) + ); + } + private _segmentClick() { if (this._hueSegments === 24 && this._saturationSegments === 8) { this._hueSegments = 0; @@ -241,15 +439,90 @@ class MoreInfoLight extends LitElement { } } + 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] *= ratio; + rgbColor[1] *= ratio; + rgbColor[2] *= ratio; + } + return rgbColor; + } + + private _setRgbColor(rgbColor: [number, number, number]) { + if (lightSupportsColorMode(this.stateObj!, LightColorModes.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!, LightColorModes.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) { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], - }); + if ( + lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW) || + lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW) + ) { + this._setRgbColor( + this._colorBrightnessSliderValue + ? this._adjustColorBrightness( + [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b], + this._colorBrightnessSliderValue + ) + : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] + ); + } else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGB)) { + const rgb_color = [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] as [ + number, + number, + number + ]; + 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(): CSSResult { @@ -275,11 +548,18 @@ class MoreInfoLight extends LitElement { ); /* 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; + display: flex; + justify-content: center; + } + + ha-button-toggle-group { + margin: 8px 0px; } ha-color-picker { @@ -293,12 +573,19 @@ class MoreInfoLight extends LitElement { .segmentationButton { position: absolute; top: 5%; + left: 0; color: var(--secondary-text-color); } paper-item { cursor: pointer; } + + hr { + border-color: var(--divider-color); + border-bottom: none; + margin: 8px 0; + } `; } } diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 2a0fc4dcc6..2c535a8bba 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -28,7 +28,7 @@ import { stateIcon } from "../../../common/entity/state_icon"; import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { iconColorCSS } from "../../../common/style/icon_color_css"; import "../../../components/ha-card"; -import { LightEntity } from "../../../data/light"; +import { getLightRgbColor, LightEntity } from "../../../data/light"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -301,14 +301,14 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { } private _computeColor(stateObj: HassEntity | LightEntity): string { - if (!stateObj.attributes.hs_color || !this._config?.state_color) { + if ( + !this._config?.state_color || + computeStateDomain(stateObj) !== "light" + ) { return ""; } - const [hue, sat] = stateObj.attributes.hs_color; - if (sat <= 10) { - return ""; - } - return `hsl(${hue}, 100%, ${100 - sat / 2}%)`; + const rgb = getLightRgbColor(stateObj as LightEntity); + return rgb ? `rgb(${rgb.slice(0, 3).join(",")})` : ""; } private _handleAction(ev: ActionHandlerEvent) { diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 521cc5c7b8..c92d6dbbca 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -18,11 +18,14 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateIcon } from "../../../common/entity/state_icon"; -import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; -import { LightEntity, SUPPORT_BRIGHTNESS } from "../../../data/light"; +import { + getLightRgbColor, + LightEntity, + lightSupportsDimming, +} from "../../../data/light"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -121,17 +124,14 @@ export class HuiLightCard extends LitElement implements LovelaceCard { @value-changing=${this._dragEvent} @value-changed=${this._setBrightness} style=${styleMap({ - visibility: supportsFeature(stateObj, SUPPORT_BRIGHTNESS) + visibility: lightSupportsDimming(stateObj) ? "visible" : "hidden", })} >