Use supported_color_modes to determine what UI elements to show (#8961)

This commit is contained in:
Bram Kragten 2021-04-27 19:43:25 +02:00 committed by GitHub
parent 20c351949f
commit 36586b798e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 587 additions and 120 deletions

View File

@ -9,13 +9,10 @@ import {
} from "lit-element"; } from "lit-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { import {
SUPPORT_BRIGHTNESS, LightColorModes,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_EFFECT,
SUPPORT_FLASH, SUPPORT_FLASH,
SUPPORT_TRANSITION, SUPPORT_TRANSITION,
SUPPORT_WHITE_VALUE,
} from "../../../src/data/light"; } from "../../../src/data/light";
import "../../../src/dialogs/more-info/more-info-content"; import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity"; import { getEntity } from "../../../src/fake_data/entity";
@ -32,7 +29,8 @@ const ENTITIES = [
getEntity("light", "kitchen_light", "on", { getEntity("light", "kitchen_light", "on", {
friendly_name: "Brightness Light", friendly_name: "Brightness Light",
brightness: 200, brightness: 200,
supported_features: SUPPORT_BRIGHTNESS, supported_color_modes: [LightColorModes.BRIGHTNESS],
color_mode: LightColorModes.BRIGHTNESS,
}), }),
getEntity("light", "color_temperature_light", "on", { getEntity("light", "color_temperature_light", "on", {
friendly_name: "White Color Temperature Light", friendly_name: "White Color Temperature Light",
@ -40,20 +38,96 @@ const ENTITIES = [
color_temp: 75, color_temp: 75,
min_mireds: 30, min_mireds: 30,
max_mireds: 150, 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", { getEntity("light", "color_hs_light", "on", {
friendly_name: "Color Effets Light", friendly_name: "Color HS Light",
brightness: 255, brightness: 255,
hs_color: [30, 100], hs_color: [30, 100],
white_value: 36, rgb_color: [30, 100, 255],
supported_features: min_mireds: 30,
SUPPORT_BRIGHTNESS + max_mireds: 150,
SUPPORT_EFFECT + supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
SUPPORT_FLASH + supported_color_modes: [
SUPPORT_COLOR + LightColorModes.BRIGHTNESS,
SUPPORT_TRANSITION + LightColorModes.COLOR_TEMP,
SUPPORT_WHITE_VALUE, 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"], effect_list: ["random", "colorloop"],
}), }),
]; ];

View File

@ -102,3 +102,18 @@ export const lab2hex = (lab: [number, number, number]): string => {
const rgb = lab2rgb(lab); const rgb = lab2rgb(lab);
return rgb2hex(rgb); 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];
};

View File

@ -15,6 +15,7 @@ import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon"; import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css"; import { iconColorCSS } from "../../common/style/icon_color_css";
import { getLightRgbColor, LightEntity } from "../../data/light";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-icon"; import "../ha-icon";
@ -99,11 +100,13 @@ export class StateBadge extends LitElement {
hostStyle.backgroundImage = `url(${imageUrl})`; hostStyle.backgroundImage = `url(${imageUrl})`;
this._showIcon = false; this._showIcon = false;
} else if (stateObj.state === "on") { } else if (stateObj.state === "on") {
if (stateObj.attributes.hs_color && this.stateColor !== false) { if (
const hue = stateObj.attributes.hs_color[0]; computeStateDomain(stateObj) === "light" &&
const sat = stateObj.attributes.hs_color[1]; this.stateColor !== false
if (sat > 10) { ) {
iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`; const rgb = getLightRgbColor(stateObj as LightEntity);
if (rgb) {
iconStyle.color = `rgb(${rgb.slice(0, 3).join(",")})`;
} }
} }
if (stateObj.attributes.brightness && this.stateColor !== false) { if (stateObj.attributes.brightness && this.stateColor !== false) {

View File

@ -9,6 +9,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
@ -19,6 +20,8 @@ export class HaButtonToggleGroup extends LitElement {
@property() public active?: string; @property() public active?: string;
@property({ type: Boolean }) public fullWidth = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div> <div>
@ -33,6 +36,11 @@ export class HaButtonToggleGroup extends LitElement {
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon> <ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button>` </mwc-icon-button>`
: html`<mwc-button : html`<mwc-button
style=${styleMap({
width: this.fullWidth
? `${100 / this.buttons.length}%`
: "initial",
})}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}

View File

@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
import { rgb2hs } from "../common/color/convert-color";
/** /**
* Color-picker custom element * Color-picker custom element
* *
@ -114,6 +114,12 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
observer: "applyHsColor", observer: "applyHsColor",
}, },
// use these properties to update the state via attributes
desiredRgbColor: {
type: Object,
observer: "applyRgbColor",
},
// width, height and radius apply to the coordinates of // width, height and radius apply to the coordinates of
// of the canvas. // of the canvas.
// border width are relative to these numbers // border width are relative to these numbers
@ -177,8 +183,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
this.drawMarker(); this.drawMarker();
if (this.desiredHsColor) { if (this.desiredHsColor) {
this.setMarkerOnColor(this.desiredHsColor); this.applyHsColor(this.desiredHsColor);
this.applyColorToCanvas(this.desiredHsColor); }
if (this.desiredRgbColor) {
this.applyRgbColor(this.desiredRgbColor);
} }
this.interactionLayer.addEventListener("mousedown", (ev) => this.interactionLayer.addEventListener("mousedown", (ev) =>
@ -282,12 +291,13 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
processUserSelect(ev) { processUserSelect(ev) {
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
const hs = this.getColor(canvasXY.x, canvasXY.y); 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 // apply color to marker position and canvas
onColorSelect(hs) { onColorSelect(hs, rgb) {
this.setMarkerOnColor(hs); // marker always follows mounse 'raw' hs value (= mouse position) this.setMarkerOnColor(hs); // marker always follows mouse 'raw' hs value (= mouse position)
if (!this.ignoreSegments) { if (!this.ignoreSegments) {
// apply segments if needed // apply segments if needed
hs = this.applySegmentFilter(hs); hs = this.applySegmentFilter(hs);
@ -301,11 +311,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// eventually after throttle limit has passed // eventually after throttle limit has passed
clearTimeout(this.ensureFinalSelect); clearTimeout(this.ensureFinalSelect);
this.ensureFinalSelect = setTimeout(() => { this.ensureFinalSelect = setTimeout(() => {
this.fireColorSelected(hs); // do it for the final time this.fireColorSelected(hs, rgb); // do it for the final time
}, this.throttle); }, this.throttle);
return; return;
} }
this.fireColorSelected(hs); // do it this.fireColorSelected(hs, rgb); // do it
this.colorSelectIsThrottled = true; this.colorSelectIsThrottled = true;
setTimeout(() => { setTimeout(() => {
this.colorSelectIsThrottled = false; this.colorSelectIsThrottled = false;
@ -313,9 +323,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
} }
// set color values and fire colorselected event // set color values and fire colorselected event
fireColorSelected(hs) { fireColorSelected(hs, rgb) {
this.hsColor = hs; 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); this.applyColorToCanvas(hs);
} }
applyRgbColor(rgb) {
const [h, s] = rgb2hs(rgb);
this.applyHsColor({ h, s });
}
/* /*
* input processing helpers * input processing helpers
*/ */
@ -395,6 +410,15 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
return { h: hue, s: sat }; 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) { applySegmentFilter(hs) {
// apply hue segment steps // apply hue segment steps
if (this.hueSegments) { if (this.hueSegments) {
@ -468,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
.getPropertyValue("--wheel-bordercolor") .getPropertyValue("--wheel-bordercolor")
.trim(); .trim();
const wheelShadow = wheelStyle.getPropertyValue("--wheel-shadow").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" // the shadow should be defined as: "10px 5px 5px 0px COLOR"
if (wheelShadow !== "none") { if (wheelShadow !== "none") {
const values = wheelShadow.split("px "); const values = wheelShadow.split("px ");

View File

@ -3,26 +3,82 @@ import {
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } 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 { interface LightEntityAttributes extends HassEntityAttributeBase {
min_mireds: number; min_mireds: number;
max_mireds: number; max_mireds: number;
friendly_name: string; friendly_name: string;
brightness: number; 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; color_temp: number;
white_value: number;
effect?: string; effect?: string;
effect_list: string[] | null; effect_list: string[] | null;
supported_color_modes: LightColorModes[];
color_mode: LightColorModes;
} }
export interface LightEntity extends HassEntityBase { export interface LightEntity extends HassEntityBase {
attributes: LightEntityAttributes; 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;

View File

@ -11,7 +11,6 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes"; import "../../../components/ha-attributes";
import "../../../components/ha-color-picker"; import "../../../components/ha-color-picker";
@ -19,20 +18,22 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-labeled-slider"; import "../../../components/ha-labeled-slider";
import "../../../components/ha-paper-dropdown-menu"; import "../../../components/ha-paper-dropdown-menu";
import { import {
getLightRgbColor,
LightColorModes,
LightEntity, LightEntity,
SUPPORT_BRIGHTNESS, lightIsInColorMode,
SUPPORT_COLOR, lightSupportsColor,
SUPPORT_COLOR_TEMP, lightSupportsColorMode,
lightSupportsDimming,
SUPPORT_EFFECT, SUPPORT_EFFECT,
SUPPORT_WHITE_VALUE,
} from "../../../data/light"; } from "../../../data/light";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-button-toggle-group";
interface HueSatColor { const toggleButtons = [
h: number; { label: "Color", value: "color" },
s: number; { label: "Temperature", value: LightColorModes.COLOR_TEMP },
} ];
@customElement("more-info-light") @customElement("more-info-light")
class MoreInfoLight extends LitElement { class MoreInfoLight extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -41,28 +42,51 @@ class MoreInfoLight extends LitElement {
@internalProperty() private _brightnessSliderValue = 0; @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 _hueSegments = 24;
@internalProperty() private _saturationSegments = 8; @internalProperty() private _saturationSegments = 8;
@internalProperty() private _colorPickerColor?: HueSatColor; @internalProperty() private _colorPickerColor?: [number, number, number];
@internalProperty() private _mode?: "color" | LightColorModes.COLOR_TEMP;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return html``; 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` return html`
<div <div class="content">
class="content ${classMap({ ${lightSupportsDimming(this.stateObj)
"is-on": this.stateObj.state === "on",
})}"
>
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
? html` ? html`
<ha-labeled-slider <ha-labeled-slider
caption=${this.hass.localize("ui.card.light.brightness")} caption=${this.hass.localize("ui.card.light.brightness")}
@ -77,7 +101,17 @@ class MoreInfoLight extends LitElement {
: ""} : ""}
${this.stateObj.state === "on" ${this.stateObj.state === "on"
? html` ? html`
${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP) ${supportsTemp || supportsColor ? html`<hr></hr>` : ""}
${supportsTemp && supportsColor
? html`<ha-button-toggle-group
fullWidth
.buttons=${toggleButtons}
.active=${this._mode}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>`
: ""}
${supportsTemp &&
(!supportsColor || this._mode === LightColorModes.COLOR_TEMP)
? html` ? html`
<ha-labeled-slider <ha-labeled-slider
class="color_temp" class="color_temp"
@ -91,27 +125,16 @@ class MoreInfoLight extends LitElement {
@change=${this._ctSliderChanged} @change=${this._ctSliderChanged}
pin pin
></ha-labeled-slider> ></ha-labeled-slider>
<hr></hr>
` `
: ""} : ""}
${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE) ${supportsColor && (!supportsTemp || this._mode === "color")
? html`
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.white_value")}
icon="hass:file-word-box"
max="255"
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_COLOR)
? html` ? html`
<div class="segmentationContainer"> <div class="segmentationContainer">
<ha-color-picker <ha-color-picker
class="color" class="color"
@colorselected=${this._colorPicked} @colorselected=${this._colorPicked}
.desiredHsColor=${this._colorPickerColor} .desiredRgbColor=${this._colorPickerColor}
throttle="500" throttle="500"
.hueSegments=${this._hueSegments} .hueSegments=${this._hueSegments}
.saturationSegments=${this._saturationSegments} .saturationSegments=${this._saturationSegments}
@ -123,6 +146,67 @@ class MoreInfoLight extends LitElement {
class="segmentationButton" class="segmentationButton"
></ha-icon-button> ></ha-icon-button>
</div> </div>
${
supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.color_brightness"
)}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue ?? 255}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: ""
}
${
supportsRgbw
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""
}
${
supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.cold_white_value"
)}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.warm_white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""
}
<hr></hr>
` `
: ""} : ""}
${supportsFeature(this.stateObj, SUPPORT_EFFECT) && ${supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
@ -151,32 +235,83 @@ class MoreInfoLight extends LitElement {
: ""} : ""}
<ha-attributes <ha-attributes
.stateObj=${this.stateObj} .stateObj=${this.stateObj}
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode" extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,rgbw_color,rgbww_color,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode"
></ha-attributes> ></ha-attributes>
</div> </div>
`; `;
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues<this>) {
if (!changedProps.has("stateObj")) {
return;
}
const stateObj = this.stateObj! as LightEntity; const stateObj = this.stateObj! as LightEntity;
if (changedProps.has("stateObj")) { const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
if (stateObj.state === "on") { 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._brightnessAdjusted = undefined;
}
this._brightnessSliderValue = Math.round( this._brightnessSliderValue = Math.round(
(stateObj.attributes.brightness * 100) / 255 (stateObj.attributes.brightness * brightnessAdjust) / 255
); );
this._ctSliderValue = stateObj.attributes.color_temp; this._ctSliderValue = stateObj.attributes.color_temp;
this._wvSliderValue = stateObj.attributes.white_value; 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;
if (stateObj.attributes.hs_color) { this._colorPickerColor = getLightRgbColor(stateObj)?.slice(0, 3) as
this._colorPickerColor = { | [number, number, number]
h: stateObj.attributes.hs_color[0], | undefined;
s: stateObj.attributes.hs_color[1] / 100,
};
}
} else { } else {
this._brightnessSliderValue = 0; this._brightnessSliderValue = 0;
} }
} }
private _modeChanged(ev: CustomEvent) {
this._mode = ev.detail.value;
} }
private _effectChanged(ev: CustomEvent) { private _effectChanged(ev: CustomEvent) {
@ -193,12 +328,29 @@ class MoreInfoLight extends LitElement {
} }
private _brightnessSliderChanged(ev: CustomEvent) { private _brightnessSliderChanged(ev: CustomEvent) {
const bri = parseInt((ev.target as any).value, 10); const bri = Number((ev.target as any).value);
if (isNaN(bri)) { if (isNaN(bri)) {
return; 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", { this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id, entity_id: this.stateObj!.entity_id,
brightness_pct: bri, brightness_pct: bri,
@ -206,7 +358,7 @@ class MoreInfoLight extends LitElement {
} }
private _ctSliderChanged(ev: CustomEvent) { private _ctSliderChanged(ev: CustomEvent) {
const ct = parseInt((ev.target as any).value, 10); const ct = Number((ev.target as any).value);
if (isNaN(ct)) { if (isNaN(ct)) {
return; return;
@ -219,16 +371,62 @@ class MoreInfoLight extends LitElement {
} }
private _wvSliderChanged(ev: CustomEvent) { 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)) { if (isNaN(wv)) {
return; 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", { this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id, entity_id: this.stateObj!.entity_id,
white_value: wv, 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;
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() { private _segmentClick() {
@ -241,16 +439,91 @@ 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. * Called when a new color has been picked.
* should be throttled with the 'throttle=' attribute of the color picker * should be throttled with the 'throttle=' attribute of the color picker
*/ */
private _colorPicked(ev: CustomEvent) { private _colorPicked(ev: CustomEvent) {
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", { this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id, entity_id: this.stateObj!.entity_id,
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
}); });
} }
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
@ -275,11 +548,18 @@ class MoreInfoLight extends LitElement {
); );
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */ /* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color); --paper-slider-knob-start-border-color: var(--primary-color);
margin-bottom: 4px;
} }
.segmentationContainer { .segmentationContainer {
position: relative; position: relative;
max-height: 500px; max-height: 500px;
display: flex;
justify-content: center;
}
ha-button-toggle-group {
margin: 8px 0px;
} }
ha-color-picker { ha-color-picker {
@ -293,12 +573,19 @@ class MoreInfoLight extends LitElement {
.segmentationButton { .segmentationButton {
position: absolute; position: absolute;
top: 5%; top: 5%;
left: 0;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
paper-item { paper-item {
cursor: pointer; cursor: pointer;
} }
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 8px 0;
}
`; `;
} }
} }

View File

@ -28,7 +28,7 @@ import { stateIcon } from "../../../common/entity/state_icon";
import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css"; import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { LightEntity } from "../../../data/light"; import { getLightRgbColor, LightEntity } from "../../../data/light";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; 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 { private _computeColor(stateObj: HassEntity | LightEntity): string {
if (!stateObj.attributes.hs_color || !this._config?.state_color) { if (
!this._config?.state_color ||
computeStateDomain(stateObj) !== "light"
) {
return ""; return "";
} }
const [hue, sat] = stateObj.attributes.hs_color; const rgb = getLightRgbColor(stateObj as LightEntity);
if (sat <= 10) { return rgb ? `rgb(${rgb.slice(0, 3).join(",")})` : "";
return "";
}
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
} }
private _handleAction(ev: ActionHandlerEvent) { private _handleAction(ev: ActionHandlerEvent) {

View File

@ -18,11 +18,14 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon"; import { stateIcon } from "../../../common/entity/state_icon";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; 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 { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
@ -121,17 +124,14 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
@value-changing=${this._dragEvent} @value-changing=${this._dragEvent}
@value-changed=${this._setBrightness} @value-changed=${this._setBrightness}
style=${styleMap({ style=${styleMap({
visibility: supportsFeature(stateObj, SUPPORT_BRIGHTNESS) visibility: lightSupportsDimming(stateObj)
? "visible" ? "visible"
: "hidden", : "hidden",
})} })}
></round-slider> ></round-slider>
<ha-icon-button <ha-icon-button
class="light-button ${classMap({ class="light-button ${classMap({
"slider-center": supportsFeature( "slider-center": lightSupportsDimming(stateObj),
stateObj,
SUPPORT_BRIGHTNESS
),
"state-on": stateObj.state === "on", "state-on": stateObj.state === "on",
"state-unavailable": stateObj.state === UNAVAILABLE, "state-unavailable": stateObj.state === UNAVAILABLE,
})}" })}"
@ -244,14 +244,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
} }
private _computeColor(stateObj: LightEntity): string { private _computeColor(stateObj: LightEntity): string {
if (stateObj.state === "off" || !stateObj.attributes.hs_color) { if (stateObj.state === "off") {
return ""; return "";
} }
const [hue, sat] = stateObj.attributes.hs_color; const rgb = getLightRgbColor(stateObj);
if (sat <= 10) { return rgb ? `rgb(${rgb.slice(0, 3).join(",")})` : "";
return "";
}
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
} }
private _handleAction(ev: ActionHandlerEvent) { private _handleAction(ev: ActionHandlerEvent) {

View File

@ -172,7 +172,10 @@
"light": { "light": {
"brightness": "Brightness", "brightness": "Brightness",
"color_temperature": "Color temperature", "color_temperature": "Color temperature",
"white_value": "White value", "white_value": "White brightness",
"color_brightness": "Color brightness",
"cold_white_value": "Cold white brightness",
"warm_white_value": "Warm white brightness",
"effect": "Effect" "effect": "Effect"
}, },
"lock": { "lock": {