button-card/src/button-card.ts

476 lines
14 KiB
TypeScript

import {
LitElement,
html,
customElement,
property,
TemplateResult,
CSSResult,
PropertyValues,
} from 'lit-element';
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
import {
HassEntity,
} from 'home-assistant-js-websocket';
import domainIcon from './domain_icons';
import {
ButtonCardConfig,
HomeAssistant,
StateConfig,
CssStyleConfig,
} from './types';
import {
computeDomain,
computeEntity,
getFontColorBasedOnBackgroundColor,
buildNameStateConcat,
applyBrightnessToColor,
hasConfigOrEntityChanged,
} from './helpers';
import { handleClick } from './handle-click';
import { longPress } from './long-press';
import { styles } from './styles';
@customElement('button-card')
class ButtonCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() private config?: ButtonCardConfig;
static get styles(): CSSResult {
return styles;
}
protected render(): TemplateResult | void {
if (!this.config || !this.hass) {
return html``;
}
return this._cardHtml();
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
private _getMatchingConfigState(state: HassEntity | undefined): StateConfig | undefined {
if (!state || !this.config!.state) {
return undefined;
}
let def: StateConfig | undefined;
const retval = this.config!.state.find((elt) => {
if (elt.operator) {
switch (elt.operator) {
case '==':
/* eslint eqeqeq: 0 */
return (state!.state == elt.value);
case '<=':
return (state!.state <= elt.value);
case '<':
return (state!.state < elt.value);
case '>=':
return (state!.state >= elt.value);
case '>':
return (state.state > elt.value);
case '!=':
return (state.state != elt.value);
case 'regex': {
/* eslint no-unneeded-ternary: 0 */
const matches = state.state.match(elt.value) ? true : false;
return matches;
}
case 'default':
def = elt;
return false;
default:
return false;
}
} else {
return (elt.value == state.state);
}
});
if (!retval && def) {
return def;
}
return retval;
}
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 _buildCssColorAttribute(
state: HassEntity | undefined, configState: StateConfig | undefined,
): string {
let colorValue: string = '';
let color: undefined | string;
if (configState && configState.color) {
colorValue = configState.color;
} else if (this.config!.color !== 'auto' && state && state.state === 'off') {
colorValue = this.config!.color_off;
} else if (this.config!.color) {
colorValue = this.config!.color;
}
if (colorValue == 'auto') {
if (state) {
if (state.attributes.rgb_color) {
color = `rgb(${state.attributes.rgb_color.join(',')})`;
if (state.attributes.brightness) {
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
}
} else if (state.attributes.brightness) {
color = applyBrightnessToColor(
this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5,
);
} else {
color = this._getDefaultColorForState(state);
}
} else {
color = this.config!.default_color;
}
} else if (colorValue) {
color = colorValue;
} else if (state) {
color = this._getDefaultColorForState(state);
} else {
color = this.config!.default_color;
}
return color;
}
private _buildIcon(
state: HassEntity | undefined, configState: StateConfig | undefined,
): string | undefined {
if (!this.config!.show_icon) {
return undefined;
}
let icon: undefined | string;
if (configState && configState.icon) {
icon = configState.icon;
} else if (this.config!.icon) {
icon = this.config!.icon;
} else if (state && state.attributes) {
icon = state.attributes.icon
? state.attributes.icon
: domainIcon(computeDomain(state.entity_id), state.state);
}
return icon;
}
private _buildEntityPicture(
state: HassEntity | undefined, configState: StateConfig | undefined,
): string | undefined {
if (!this.config!.show_entity_picture
|| !state && !configState && !this.config!.entity_picture) {
return undefined;
}
let entityPicture: string | undefined;
if (configState && configState.entity_picture) {
entityPicture = configState.entity_picture;
} else if (this.config!.entity_picture) {
entityPicture = this.config!.entity_picture;
} else {
entityPicture = state && state.attributes && state.attributes.entity_picture
? state.attributes.entity_picture : undefined;
}
return entityPicture;
}
private _buildStyle(
state: HassEntity | undefined, configState: StateConfig | undefined,
): StyleInfo {
let cardStyle: StyleInfo = {};
let styleArray: CssStyleConfig[] | undefined;
if (state) {
if (configState && configState.style) {
styleArray = configState.style;
} else if (this.config!.style) {
styleArray = this.config!.style;
}
} else if (this.config!.style) {
styleArray = this.config!.style;
}
if (styleArray) {
cardStyle = Object.assign(cardStyle, ...styleArray);
}
return cardStyle;
}
private _buildEntityPictureStyle(
state: HassEntity | undefined, configState: StateConfig | undefined,
): StyleInfo {
let entityPictureStyle: StyleInfo = {};
let styleArray: CssStyleConfig[] | undefined;
if (state) {
if (configState && configState.entity_picture_style) {
styleArray = configState.entity_picture_style;
} else if (this.config!.entity_picture_style) {
styleArray = this.config!.entity_picture_style;
}
} else if (this.config!.entity_picture_style) {
styleArray = this.config!.entity_picture_style;
}
if (styleArray) {
entityPictureStyle = Object.assign(entityPictureStyle, ...styleArray);
}
return entityPictureStyle;
}
private _buildName(
state: HassEntity | undefined, configState: StateConfig | undefined,
): string | undefined {
if (this.config!.show_name === false) {
return undefined;
}
let name: string | undefined;
if (configState && configState.name) {
name = configState.name;
} else if (this.config!.name) {
name = this.config!.name;
} else if (state) {
name = state.attributes && state.attributes.friendly_name
? state.attributes.friendly_name : computeEntity(state.entity_id);
}
return name;
}
private _buildStateString(state: HassEntity | undefined): string | undefined {
let stateString: string | undefined;
if (this.config!.show_state && state && state.state) {
const units = this._buildUnits(state);
if (units) {
stateString = `${state.state} ${units}`;
} else {
stateString = state.state;
}
}
return stateString;
}
private _buildUnits(state: HassEntity | undefined): string | undefined {
let units: string | undefined;
if (state) {
if (this.config!.show_units) {
if (state.attributes && state.attributes.unit_of_measurement && !this.config!.units) {
units = state.attributes.unit_of_measurement;
} else {
units = this.config!.units ? this.config!.units : undefined;
}
}
}
return units;
}
private _isClickable(state: HassEntity | undefined): boolean {
let clickable = true;
if (this.config!.tap_action!.action === 'toggle' && this.config!.hold_action!.action === 'none'
|| this.config!.hold_action!.action === 'toggle' && this.config!.tap_action!.action === 'none') {
if (state) {
switch (computeDomain(state.entity_id)) {
case 'sensor':
case 'binary_sensor':
case 'device_tracker':
clickable = false;
break;
default:
clickable = true;
break;
}
} else {
clickable = false;
}
} else if (this.config!.tap_action!.action != 'none'
|| this.config!.hold_action!.action != 'none') {
clickable = true;
} else {
clickable = false;
}
return clickable;
}
private _rotate(configState: StateConfig | undefined): Boolean {
return configState && configState.spin ? true : false;
}
private _blankCardColoredHtml(state: HassEntity | undefined): TemplateResult {
const color = this._buildCssColorAttribute(state, undefined);
const fontColor = getFontColorBasedOnBackgroundColor(color);
return html`
<ha-card class="disabled">
<div style="color: ${fontColor}; background-color: ${color};"></div>
</ha-card>
`;
}
private _cardHtml(): TemplateResult {
const state = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined;
const configState = this._getMatchingConfigState(state);
const color = this._buildCssColorAttribute(state, configState);
let buttonColor = color;
let cardStyle: StyleInfo = {};
const configCardStyle = this._buildStyle(state, configState);
switch (this.config!.color_type) {
case 'blank-card':
return this._blankCardColoredHtml(state);
case 'card':
case 'label-card': {
const fontColor = getFontColorBasedOnBackgroundColor(color);
cardStyle.color = fontColor;
cardStyle['background-color'] = color;
cardStyle = { ...cardStyle, ...configCardStyle };
buttonColor = 'inherit';
break;
}
default:
cardStyle = configCardStyle;
break;
}
if (configCardStyle.width) {
this.style.setProperty('flex', '0 0 auto');
this.style.setProperty('max-width', 'fit-content');
}
return html`
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
${this._buttonContent(state, configState, buttonColor)}
<mwc-ripple></mwc-ripple>
</ha-card>
`;
}
private _buttonContent(
state: HassEntity | undefined,
configState: StateConfig | undefined,
color: string,
): TemplateResult {
const name = this._buildName(state, configState);
const stateString = this._buildStateString(state);
const nameStateString = buildNameStateConcat(name, stateString);
switch (this.config!.layout) {
case 'icon_name_state':
case 'name_state':
return this._gridHtml(state, configState, this.config!.layout, color,
nameStateString, undefined);
default:
return this._gridHtml(state, configState, this.config!.layout, color,
name, stateString);
}
}
private _gridHtml(
state: HassEntity | undefined,
configState: StateConfig | undefined,
containerClass: string,
color: string,
name: string | undefined,
stateString: string | undefined,
): TemplateResult {
const iconTemplate = this._getIconHtml(state, configState, color);
const itemClass: string[] = ['container', containerClass];
if (!iconTemplate) itemClass.push('no-icon');
if (!name) itemClass.push('no-name');
if (!stateString) itemClass.push('no-state');
return html`
<div class=${itemClass.join(' ')}>
${iconTemplate ? iconTemplate : ''}
${name ? html`<div class="name">${name}</div>` : ''}
${stateString ? html`<div class="state">${stateString}</div>` : ''}
</div>
`;
}
private _getIconHtml(
state: HassEntity | undefined,
configState: StateConfig | undefined,
color: string,
): TemplateResult | undefined {
const icon = this._buildIcon(state, configState);
const entityPicture = this._buildEntityPicture(state, configState);
const entityPictureStyleFromConfig = this._buildEntityPictureStyle(state, configState);
const haIconStyle = {
color,
width: this.config!.size,
'min-width': this.config!.size,
};
const entityPictureStyle = {
...haIconStyle,
...entityPictureStyleFromConfig,
};
if (icon || entityPicture) {
return html`
<div class="img-cell">
${icon && !entityPicture ? html`<ha-icon style=${styleMap(haIconStyle)}
.icon="${icon}" class="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
${entityPicture ? html`<img src="${entityPicture}" style=${styleMap(entityPictureStyle)}
class="icon" ?rotating=${this._rotate(configState)} />` : ''}
</div>
`;
} else {
return undefined;
}
}
public setConfig(config: ButtonCardConfig): void {
if (!config) {
throw new Error('Invalid configuration');
}
this.config = {
tap_action: { action: 'toggle' },
hold_action: { action: 'none' },
layout: 'vertical',
size: '40%',
color_type: 'icon',
show_name: true,
show_state: false,
show_icon: true,
show_units: true,
show_entity_picture: false,
...config,
};
this.config!.default_color = 'var(--primary-text-color)';
if (this.config!.color_type !== 'icon') {
this.config!.color_off = 'var(--paper-card-background-color)';
} else {
this.config!.color_off = 'var(--paper-item-icon-color)';
}
this.config!.color_on = 'var(--paper-item-icon-active-color)';
}
// The height of your card. Home Assistant uses this to automatically
// distribute all cards over the available columns.
public getCardSize(): number {
return 3;
}
private _handleTap(ev): void {
/* eslint no-alert: 0 */
if (this.config!.confirmation
&& !window.confirm(this.config!.confirmation)) {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, false);
}
private _handleHold(ev): void {
/* eslint no-alert: 0 */
if (this.config!.confirmation
&& !window.confirm(this.config!.confirmation)) {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, true);
}
}