fix!: Color are now aligned with HA > 2022.12
BREAKING CHANGE: this might break some of your color settings Fix #635
This commit is contained in:
parent
0b3e4d331c
commit
685d55e49c
|
@ -58,6 +58,8 @@ import { myComputeStateDisplay } from './compute_state_display';
|
|||
import copy from 'fast-copy';
|
||||
import * as pjson from '../package.json';
|
||||
import { deepEqual } from './deep-equal';
|
||||
import { stateColorCss } from './state_color';
|
||||
import { ON } from './const';
|
||||
|
||||
let helpers = (window as any).cardHelpers;
|
||||
const helperPromise = new Promise(async (resolve) => {
|
||||
|
@ -337,21 +339,10 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private _getDefaultColorForState(state: HassEntity): string {
|
||||
switch (state.state) {
|
||||
case 'on':
|
||||
return this._config!.color_on;
|
||||
case 'off':
|
||||
return this._config!.color_off;
|
||||
default:
|
||||
return this._config!.default_color;
|
||||
}
|
||||
}
|
||||
|
||||
private _getColorForLightEntity(state: HassEntity | undefined, useTemperature: boolean): string {
|
||||
let color: string = this._config!.default_color;
|
||||
if (state) {
|
||||
if (state.state === 'on') {
|
||||
if (state.state === ON) {
|
||||
if (state.attributes.rgb_color) {
|
||||
color = `rgb(${state.attributes.rgb_color.join(',')})`;
|
||||
if (state.attributes.brightness) {
|
||||
|
@ -372,12 +363,15 @@ class ButtonCard extends LitElement {
|
|||
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||
}
|
||||
} else if (state.attributes.brightness) {
|
||||
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
|
||||
color = applyBrightnessToColor(
|
||||
stateColorCss(state, state.state) || this._config!.default_color,
|
||||
(state.attributes.brightness + 245) / 5,
|
||||
);
|
||||
} else {
|
||||
color = this._getDefaultColorForState(state);
|
||||
color = stateColorCss(state, state.state) || this._config!.default_color;
|
||||
}
|
||||
} else {
|
||||
color = this._getDefaultColorForState(state);
|
||||
color = stateColorCss(state, state.state) || this._config!.default_color;
|
||||
}
|
||||
}
|
||||
return color;
|
||||
|
@ -388,8 +382,6 @@ class ButtonCard extends LitElement {
|
|||
let color: undefined | string;
|
||||
if (configState?.color) {
|
||||
colorValue = configState.color;
|
||||
} else if (this._config!.color !== 'auto' && state?.state === 'off') {
|
||||
colorValue = this._config!.color_off;
|
||||
} else if (this._config!.color) {
|
||||
colorValue = this._config!.color;
|
||||
}
|
||||
|
@ -398,7 +390,7 @@ class ButtonCard extends LitElement {
|
|||
} else if (colorValue) {
|
||||
color = colorValue;
|
||||
} else if (state) {
|
||||
color = this._getDefaultColorForState(state);
|
||||
color = stateColorCss(state, state.state) || this._config!.default_color;
|
||||
} else {
|
||||
color = this._config!.default_color;
|
||||
}
|
||||
|
@ -982,8 +974,6 @@ class ButtonCard extends LitElement {
|
|||
card_size: 3,
|
||||
...template,
|
||||
default_color: 'DUMMY',
|
||||
color_off: 'DUMMY',
|
||||
color_on: 'DUMMY',
|
||||
lock: {
|
||||
enabled: false,
|
||||
duration: 5,
|
||||
|
@ -1003,12 +993,6 @@ class ButtonCard extends LitElement {
|
|||
};
|
||||
}
|
||||
this._config!.default_color = 'var(--primary-text-color)';
|
||||
if (this._config!.color_type !== 'icon') {
|
||||
this._config!.color_off = 'var(--card-background-color)';
|
||||
} else {
|
||||
this._config!.color_off = 'var(--paper-item-icon-color)';
|
||||
}
|
||||
this._config!.color_on = 'var(--paper-item-icon-active-color)';
|
||||
|
||||
const jsonConfig = JSON.stringify(this._config);
|
||||
this._entities = [];
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export const UNAVAILABLE = 'unavailable';
|
||||
const arrayLiteralIncludes = <T extends readonly unknown[]>(array: T) => (
|
||||
searchElement: unknown,
|
||||
fromIndex?: number,
|
||||
): searchElement is T[number] => array.includes(searchElement as T[number], fromIndex);
|
||||
|
||||
export const UNKNOWN = 'unknown';
|
||||
export const ON = 'on';
|
||||
export const OFF = 'off';
|
||||
|
||||
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN] as const;
|
||||
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
|
||||
|
||||
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
|
||||
export const isOffState = arrayLiteralIncludes(OFF_STATES);
|
175
src/helpers.ts
175
src/helpers.ts
|
@ -2,6 +2,8 @@ import { PropertyValues } from 'lit-element';
|
|||
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
||||
import { HomeAssistant, LovelaceConfig } from 'custom-card-helpers';
|
||||
import { StateConfig } from './types';
|
||||
import { HassEntity, HassEntityAttributeBase, HassEntityBase } from 'home-assistant-js-websocket';
|
||||
import { OFF, UNAVAILABLE, isUnavailableState } from './const';
|
||||
|
||||
export function computeDomain(entityId: string): string {
|
||||
return entityId.substr(0, entityId.indexOf('.'));
|
||||
|
@ -12,19 +14,49 @@ export function computeEntity(entityId: string): string {
|
|||
}
|
||||
|
||||
export function getColorFromVariable(color: string): string {
|
||||
if (color.substring(0, 3) === 'var') {
|
||||
return window.getComputedStyle(document.documentElement).getPropertyValue(color.substring(4).slice(0, -1)).trim();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const colorArray: string[] = [];
|
||||
let result = color;
|
||||
if (color.trim().substring(0, 3) === 'var') {
|
||||
color.split(',').forEach((singleColor) => {
|
||||
const matches = singleColor.match(/var\(\s*([a-zA-Z0-9-]*)/);
|
||||
if (matches) {
|
||||
colorArray.push(matches[1]);
|
||||
}
|
||||
});
|
||||
|
||||
colorArray.some((color) => {
|
||||
const root = window.getComputedStyle(document.documentElement).getPropertyValue(color);
|
||||
if (root) {
|
||||
result = root;
|
||||
return true;
|
||||
}
|
||||
const customStyles = document.documentElement.getElementsByTagName('custom-style');
|
||||
for (const element of customStyles) {
|
||||
const value = window.getComputedStyle(element).getPropertyValue(color);
|
||||
if (value) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return color;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getFontColorBasedOnBackgroundColor(backgroundColor: string): string {
|
||||
const colorObj = new TinyColor(getColorFromVariable(backgroundColor));
|
||||
if (colorObj.isValid && colorObj.getLuminance() > 0.5) {
|
||||
return 'rgb(62, 62, 62)'; // bright colors - black font
|
||||
} else {
|
||||
return 'rgb(234, 234, 234)'; // dark colors - white font
|
||||
const bgLuminance = new TinyColor(getColorFromVariable(backgroundColor)).getLuminance();
|
||||
const light = new TinyColor({ r: 225, g: 225, b: 225 }).getLuminance();
|
||||
const dark = new TinyColor({ r: 28, g: 28, b: 28 }).getLuminance();
|
||||
|
||||
if (bgLuminance === 0) {
|
||||
return 'rgb(225, 225, 225)';
|
||||
}
|
||||
|
||||
const whiteContrast = (Math.max(bgLuminance, light) + 0.05) / Math.min(bgLuminance, light + 0.05);
|
||||
const blackContrast = (Math.max(bgLuminance, dark) + 0.05) / Math.min(bgLuminance, dark + 0.05);
|
||||
return whiteContrast > blackContrast ? 'rgb(225, 225, 225)' : 'rgb(28, 28, 28)';
|
||||
}
|
||||
|
||||
export function getLightColorBasedOnTemperature(current: number, min: number, max: number): string {
|
||||
|
@ -172,3 +204,130 @@ export function getLovelace(): LovelaceConfig | null {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function slugify(value: string, delimiter = '_'): string {
|
||||
const a = 'àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;';
|
||||
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
||||
const p = new RegExp(a.split('').join('|'), 'g');
|
||||
|
||||
return value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, delimiter) // Replace spaces with delimiter
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
|
||||
.replace(/[^\w-]+/g, '') // Remove all non-word characters
|
||||
.replace(/-/g, delimiter) // Replace - with delimiter
|
||||
.replace(new RegExp(`(${delimiter})\\1+`, 'g'), '$1') // Replace multiple delimiters with single delimiter
|
||||
.replace(new RegExp(`^${delimiter}+`), '') // Trim delimiter from start of text
|
||||
.replace(new RegExp(`${delimiter}+$`), ''); // Trim delimiter from end of text
|
||||
}
|
||||
|
||||
interface GroupEntityAttributes extends HassEntityAttributeBase {
|
||||
entity_id: string[];
|
||||
order: number;
|
||||
auto?: boolean;
|
||||
view?: boolean;
|
||||
control?: 'hidden';
|
||||
}
|
||||
export interface GroupEntity extends HassEntityBase {
|
||||
attributes: GroupEntityAttributes;
|
||||
}
|
||||
|
||||
export const computeGroupDomain = (stateObj: GroupEntity): string | undefined => {
|
||||
const entityIds = stateObj.attributes.entity_id || [];
|
||||
const uniqueDomains = [...new Set(entityIds.map((entityId) => computeDomain(entityId)))];
|
||||
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
||||
};
|
||||
|
||||
export function stateActive(stateObj: HassEntity | undefined, state?: string): boolean {
|
||||
if (stateObj === undefined) {
|
||||
return false;
|
||||
}
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (['button', 'event', 'input_button', 'scene'].includes(domain)) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (isUnavailableState(compareState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The "off" check is relevant for most domains, but there are exceptions
|
||||
// such as "alert" where "off" is still a somewhat active state and
|
||||
// therefore gets a custom color and "idle" is instead the state that
|
||||
// matches what most other domains consider inactive.
|
||||
if (compareState === OFF && domain !== 'alert') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom cases
|
||||
switch (domain) {
|
||||
case 'alarm_control_panel':
|
||||
return compareState !== 'disarmed';
|
||||
case 'alert':
|
||||
// "on" and "off" are active, as "off" just means alert was acknowledged but is still active
|
||||
return compareState !== 'idle';
|
||||
case 'cover':
|
||||
return compareState !== 'closed';
|
||||
case 'device_tracker':
|
||||
case 'person':
|
||||
return compareState !== 'not_home';
|
||||
case 'lock':
|
||||
return compareState !== 'locked';
|
||||
case 'media_player':
|
||||
return compareState !== 'standby';
|
||||
case 'vacuum':
|
||||
return !['idle', 'docked', 'paused'].includes(compareState);
|
||||
case 'plant':
|
||||
return compareState === 'problem';
|
||||
case 'group':
|
||||
return ['on', 'home', 'open', 'locked', 'problem'].includes(compareState);
|
||||
case 'timer':
|
||||
return compareState === 'active';
|
||||
case 'camera':
|
||||
return compareState === 'streaming';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const batteryStateColorProperty = (state: string): string | undefined => {
|
||||
const value = Number(state);
|
||||
if (isNaN(value)) {
|
||||
return undefined;
|
||||
}
|
||||
if (value >= 70) {
|
||||
return '--state-sensor-battery-high-color';
|
||||
}
|
||||
if (value >= 30) {
|
||||
return '--state-sensor-battery-medium-color';
|
||||
}
|
||||
return '--state-sensor-battery-low-color';
|
||||
};
|
||||
|
||||
export function computeCssVariable(props: string | string[]): string | undefined {
|
||||
if (Array.isArray(props)) {
|
||||
return props
|
||||
.reverse()
|
||||
.reduce<string | undefined>((str, variable) => `var(${variable}${str ? `, ${str}` : ''})`, undefined);
|
||||
}
|
||||
return `var(${props})`;
|
||||
}
|
||||
|
||||
export function computeCssValue(prop: string | string[], computedStyles: CSSStyleDeclaration): string | undefined {
|
||||
if (Array.isArray(prop)) {
|
||||
for (const property of prop) {
|
||||
const value = computeCssValue(property, computedStyles);
|
||||
if (value) return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!prop.endsWith('-color')) {
|
||||
return undefined;
|
||||
}
|
||||
return computedStyles.getPropertyValue(prop).trim() || undefined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/** Return an color representing a state. */
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { UNAVAILABLE } from './const';
|
||||
import { computeGroupDomain, GroupEntity } from './helpers';
|
||||
import { computeCssVariable } from './helpers';
|
||||
import { computeDomain, slugify } from './helpers';
|
||||
import { batteryStateColorProperty } from './helpers';
|
||||
import { stateActive } from './helpers';
|
||||
|
||||
const STATE_COLORED_DOMAIN = new Set([
|
||||
'alarm_control_panel',
|
||||
'alert',
|
||||
'automation',
|
||||
'binary_sensor',
|
||||
'calendar',
|
||||
'camera',
|
||||
'climate',
|
||||
'cover',
|
||||
'device_tracker',
|
||||
'fan',
|
||||
'group',
|
||||
'humidifier',
|
||||
'input_boolean',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
'person',
|
||||
'plant',
|
||||
'remote',
|
||||
'schedule',
|
||||
'script',
|
||||
'siren',
|
||||
'sun',
|
||||
'switch',
|
||||
'timer',
|
||||
'update',
|
||||
'vacuum',
|
||||
]);
|
||||
|
||||
export const stateColorCss = (stateObj: HassEntity, state?: string): undefined | string => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
if (compareState === UNAVAILABLE) {
|
||||
return `var(--state-unavailable-color)`;
|
||||
}
|
||||
|
||||
const properties = stateColorProperties(stateObj, state);
|
||||
if (properties) {
|
||||
return computeCssVariable(properties);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const domainStateColorProperties = (domain: string, stateObj: HassEntity, state?: string): string[] => {
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
const active = stateActive(stateObj, state);
|
||||
|
||||
const properties: string[] = [];
|
||||
|
||||
const stateKey = slugify(compareState, '_');
|
||||
const activeKey = active ? 'active' : 'inactive';
|
||||
|
||||
const dc = stateObj.attributes.device_class;
|
||||
|
||||
if (dc) {
|
||||
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
|
||||
}
|
||||
|
||||
properties.push(
|
||||
`--state-${domain}-${stateKey}-color`,
|
||||
`--state-${domain}-${activeKey}-color`,
|
||||
`--state-${activeKey}-color`,
|
||||
);
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
export const stateColorProperties = (stateObj: HassEntity, state?: string): string[] | undefined => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const dc = stateObj.attributes.device_class;
|
||||
|
||||
// Special rules for battery coloring
|
||||
if (domain === 'sensor' && dc === 'battery') {
|
||||
const property = batteryStateColorProperty(compareState);
|
||||
if (property) {
|
||||
return [property];
|
||||
}
|
||||
}
|
||||
|
||||
// Special rules for group coloring
|
||||
if (domain === 'group') {
|
||||
const groupDomain = computeGroupDomain(stateObj as GroupEntity);
|
||||
if (groupDomain && STATE_COLORED_DOMAIN.has(groupDomain)) {
|
||||
return domainStateColorProperties(groupDomain, stateObj, state);
|
||||
}
|
||||
}
|
||||
|
||||
if (STATE_COLORED_DOMAIN.has(domain)) {
|
||||
return domainStateColorProperties(domain, stateObj, state);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const stateColorBrightness = (stateObj: HassEntity): string => {
|
||||
if (stateObj.attributes.brightness && computeDomain(stateObj.entity_id) !== 'plant') {
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
return '';
|
||||
};
|
|
@ -4,6 +4,7 @@ export const styles = css`
|
|||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--state-inactive-color: var(--paper-item-icon-color);
|
||||
}
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -34,8 +34,6 @@ export interface ButtonCardConfig {
|
|||
layout: Layout;
|
||||
entity_picture_style?: CssStyleConfig[];
|
||||
default_color: string;
|
||||
color_on: string;
|
||||
color_off: string;
|
||||
custom_fields?: CustomFields;
|
||||
variables?: Variables;
|
||||
extra_styles?: string;
|
||||
|
|
Loading…
Reference in New Issue