homeassistant-config/www/custom_ui/multiple-entity-row.js

391 lines
16 KiB
JavaScript
Raw Normal View History

2020-05-03 16:41:27 +02:00
((LitElement) => {
2021-02-01 21:02:55 +01:00
console.info(
'%c MULTIPLE-ENTITY-ROW %c 3.5.1 ',
'color: cyan; background: black; font-weight: bold;',
'color: darkblue; background: white; font-weight: bold;',
);
2020-05-03 16:41:27 +02:00
const html = LitElement.prototype.html;
const css = LitElement.prototype.css;
2021-02-01 21:02:55 +01:00
const UNAVAILABLE = 'unavailable';
const UNKNOWN = 'unknown';
2020-05-03 16:41:27 +02:00
class MultipleEntityRow extends LitElement {
static get properties() {
return {
_hass: {},
_config: {},
state: {},
}
}
static get styles() {
return css`
:host {
display: flex;
align-items: center;
}
.flex {
flex: 1;
margin-left: 16px;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 0;
}
.info {
flex: 1 0 60px;
cursor: pointer;
}
.info, .info > * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex ::slotted(*) {
margin-left: 8px;
min-width: 0;
}
.flex ::slotted([slot="secondary"]) {
margin-left: 0;
}
.secondary, ha-relative-time {
display: block;
color: var(--secondary-text-color);
}
2021-02-01 21:02:55 +01:00
hui-warning {
width: 100%;
}
2020-05-03 16:41:27 +02:00
state-badge {
flex: 0 0 40px;
cursor: pointer;
}
2021-02-01 21:02:55 +01:00
.icon-small {
width: auto;
}
2020-05-03 16:41:27 +02:00
.entity {
text-align: center;
cursor: pointer;
}
.entity span {
font-size: 10px;
color: var(--secondary-text-color);
}
2021-02-01 21:02:55 +01:00
.entities-row {
flex-direction: row;
display: inline-flex;
justify-content: space-between;
align-items: center;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
.entities-row .entity {
margin-right: 16px;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
.entities-row .entity:last-of-type {
margin-right: 0;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
.entities-column {
flex-direction: column;
display: flex;
align-items: flex-end;
justify-content: space-evenly;
}
.entities-column .entity div {
display: inline-block;
vertical-align: middle;
2020-05-03 16:41:27 +02:00
}`;
}
render() {
2021-02-01 21:02:55 +01:00
return this.state.stateObj ? html`
2020-05-03 16:41:27 +02:00
<state-badge
.stateObj="${this.state.stateObj}"
.overrideIcon="${this._config.icon}"
.stateColor="${this._config.state_color}"
@click="${this.onRowClick}">
</state-badge>
<div class="flex">
<div class="info" @click="${this.onRowClick}">
${this.state.name}
2021-02-01 21:02:55 +01:00
<div class="secondary" style="${this.state.info && this.state.info.style}">${this.renderSecondaryInfo()}</div>
</div>
<div class="${this._config.column ? 'entities-column' : 'entities-row'}">
${this.state.entities.map(entity => this.renderEntity(entity))}
${this.state.value ? html`
<div class="state entity" style="${this.state.style}" @click="${this.onRowClick}">
${this.stateHeader && html`<span>${this.stateHeader}</span>`}
<div>${this.renderMainState()}</div>
</div>` : null}
2020-05-03 16:41:27 +02:00
</div>
2021-02-01 21:02:55 +01:00
</div>` : html`
<hui-warning>
${this._hass.localize('ui.panel.lovelace.warning.entity_not_found', 'entity', this._config.entity)}
</hui-warning>`;
2020-05-03 16:41:27 +02:00
}
renderMainState() {
2021-02-01 21:02:55 +01:00
if (this.state.toggle) return this.renderToggle(this.state.stateObj);
const unit = this.state.unit && !this.state.unavailable ? ` ${this.state.unit}` : null;
return html`${this.renderValue(this.state)}${unit}`;
2020-05-03 16:41:27 +02:00
}
renderSecondaryInfo() {
2021-02-01 21:02:55 +01:00
if (this.lastChanged)
return html`<ha-relative-time datetime="${this.state.stateObj.last_changed}" .hass="${this._hass}"></ha-relative-time>`;
if (this.state.info) {
const name = this.state.info.name ? `${this.state.info.name} ` : null;
const unit = this.state.info.unit && !this.state.info.unavailable ? ` ${this.state.info.unit}` : null;
return html`${name}${this.renderValue(this.state.info)}${unit}`;
}
2020-05-03 16:41:27 +02:00
}
renderToggle(stateObj) {
return html`<ha-entity-toggle .stateObj="${stateObj}" .hass="${this._hass}"></ha-entity-toggle>`;
}
2021-02-01 21:02:55 +01:00
renderValue(entity) {
if (entity.unavailable || !entity.format) return entity.value;
if (entity.format === 'duration') return html`${this.secondsToDuration(entity.value)}`;
if (entity.format.startsWith('precision')) {
const precision = parseInt(entity.format.slice(-1), 10);
return html`${parseFloat(entity.value).toFixed(precision)}`;
}
return html`<hui-timestamp-display .ts=${new Date(entity.value)} .format=${entity.format} .hass=${this._hass}></hui-timestamp-display>`;
2020-05-03 16:41:27 +02:00
}
renderIcon(entity) {
return html`<state-badge class="icon-small" .stateObj="${entity.stateObj}" .overrideIcon="${entity.icon}" .stateColor="${entity.state_color}"></state-badge>`;
}
renderEntityValue(entity) {
if (entity.toggle) return this.renderToggle(entity.stateObj);
2021-02-01 21:02:55 +01:00
if (entity.icon !== undefined) return this.renderIcon(entity);
const unit = entity.unit && !entity.unavailable ? ` ${entity.unit}` : null;
return html`${this.renderValue(entity)}${unit}`;
2020-05-03 16:41:27 +02:00
}
renderEntity(entity) {
return entity ? html`
2021-02-01 21:02:55 +01:00
<div class="entity" style="${entity.style}" @click="${entity.onClick}">
2020-05-03 16:41:27 +02:00
<span>${entity.name}</span>
<div>${this.renderEntityValue(entity)}</div>
</div>` : null;
}
setConfig(config) {
if (!config.entity) throw new Error('Please define a main entity.');
if (config.entities) {
config.entities.map(entity => this.checkEntity(entity));
}
this.checkEntity(config.secondary_info);
this.lastChanged = config.secondary_info === 'last-changed';
this.stateHeader = config.state_header !== undefined ? config.state_header : null;
2021-02-01 21:02:55 +01:00
this.onRowClick = this.getAction(config.tap_action, config.entity);
2020-05-03 16:41:27 +02:00
this._config = config;
}
set hass(hass) {
this._hass = hass;
if (hass && this._config) {
const mainStateObj = hass.states[this._config.entity];
2021-02-01 21:02:55 +01:00
this.state = mainStateObj ? {
2020-05-03 16:41:27 +02:00
...this.state,
stateObj: mainStateObj,
name: this.entityName(this._config.name, mainStateObj),
2021-02-01 21:02:55 +01:00
value: this._config.show_state !== false ? this.entityStateValue(mainStateObj) : null,
unit: this._config.unit === false ? null : (this._config.unit || mainStateObj.attributes.unit_of_measurement),
unavailable: [UNKNOWN, UNAVAILABLE].includes(mainStateObj.state),
2020-05-03 16:41:27 +02:00
toggle: this.checkToggle(this._config, mainStateObj),
2021-02-01 21:02:55 +01:00
format: this._config.format || false,
style: this.entityStyles(this._config),
2020-05-03 16:41:27 +02:00
entities: this._config.entities ? this._config.entities.map(entity => this.initEntity(entity, mainStateObj)) : [],
info: this.lastChanged ? null :
typeof this._config.secondary_info === 'string'
? {value: this._config.secondary_info}
: this.initEntity(this._config.secondary_info, mainStateObj),
2021-02-01 21:02:55 +01:00
} : {};
2020-05-03 16:41:27 +02:00
}
}
checkEntity(config) {
if (config && typeof config === 'object' && !(config.entity || config.attribute || config.icon)) {
throw new Error(`Entity object requires at least one 'entity', 'attribute' or 'icon'.`);
} else if (config && typeof config === 'string' && config === '') {
throw new Error('Entity ID string must not be blank.');
} else if (config && typeof config !== 'string' && typeof config !== 'object') {
throw new Error('Entity config must be a valid entity ID string or entity object.');
}
}
checkToggle(config, stateObj) {
2021-02-01 21:02:55 +01:00
return config.toggle === true && stateObj.state && ![UNKNOWN, UNAVAILABLE].includes(stateObj.state)
2020-05-03 16:41:27 +02:00
}
initEntity(config, mainStateObj) {
if (!config) return null;
const entity = typeof config === 'string' ? config : config.entity;
const stateObj = entity ? (this._hass && this._hass.states[entity]) : mainStateObj;
2021-02-01 21:02:55 +01:00
if (config.hide_unavailable && (!stateObj || [UNKNOWN, UNAVAILABLE].includes(stateObj.state))) return null;
if (!stateObj) return {value: this._hass.localize('state.default.unavailable'), unavailable: true};
2020-05-03 16:41:27 +02:00
return {
stateObj: stateObj,
name: entity ? this.entityName(config.name, stateObj) : (config.name || null),
value: config.attribute !== undefined
2021-02-01 21:02:55 +01:00
? this.entityAttribute(stateObj, config.attribute)
: this.entityStateValue(stateObj),
unit: config.unit === false ? null : config.attribute !== undefined ? config.unit : (config.unit || stateObj.attributes.unit_of_measurement),
unavailable: [UNKNOWN, UNAVAILABLE].includes(config.attribute !== undefined ? stateObj.attributes[config.attribute] : stateObj.state),
2020-05-03 16:41:27 +02:00
toggle: this.checkToggle(config, stateObj),
2021-02-01 21:02:55 +01:00
icon: config.icon === true ? (stateObj.attributes.icon || null) : config.icon,
2020-05-03 16:41:27 +02:00
format: config.format || false,
state_color: config.state_color || false,
2021-02-01 21:02:55 +01:00
style: this.entityStyles(config),
2020-05-03 16:41:27 +02:00
onClick: this.getAction(config.tap_action, stateObj.entity_id),
};
}
entityName(name, stateObj) {
if (name === false) return null;
if (name !== undefined) return name;
return stateObj.attributes.friendly_name === undefined
? stateObj.entity_id.substr(stateObj.entity_id.indexOf('.') + 1).replace(/_/g, ' ')
: stateObj.attributes.friendly_name || '';
}
2021-02-01 21:02:55 +01:00
entityAttribute(stateObj, attribute) {
2020-05-03 16:41:27 +02:00
return (attribute in stateObj.attributes)
2021-02-01 21:02:55 +01:00
? stateObj.attributes[attribute]
2020-05-03 16:41:27 +02:00
: this._hass.localize('state.default.unavailable');
}
2021-02-01 21:02:55 +01:00
entityStateValue(stateObj) {
if (stateObj.state === UNKNOWN || stateObj.state === UNAVAILABLE) {
return this._hass.localize(`state.default.${stateObj.state}`);
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
const domain = stateObj.entity_id.substr(0, stateObj.entity_id.indexOf('.'));
return (
(stateObj.attributes.device_class
&& this._hass.localize(`component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}`))
|| this._hass.localize(`component.${domain}.state._.${stateObj.state}`)
|| stateObj.state
);
}
entityStyles(config) {
return config.styles && typeof config.styles === 'object'
? Object.keys(config.styles).map(key => `${key}: ${config.styles[key]};`).join('') : '';
2020-05-03 16:41:27 +02:00
}
getAction(config, entityId) {
2021-02-01 21:02:55 +01:00
if (!config || !config.action || config.action === 'more-info') {
return () => this.fireEvent(this, 'hass-more-info', {entityId: (config && config.entity) || entityId});
}
if (config.action === 'none') {
return null;
}
return () => {
if (config.confirmation) {
this.forwardHaptic('warning');
if (!confirm(config.confirmation === true ? `Are you sure?` : config.confirmation)) {
return;
}
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
switch (config.action) {
case 'call-service': {
if (!config.service) {
this.forwardHaptic('failure');
return;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
const [domain, service] = config.service.split('.');
this._hass.callService(domain, service, config.service_data);
this.forwardHaptic('light');
break;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
case 'toggle': {
this.toggleEntity(entityId);
this.forwardHaptic('light');
break;
}
case 'url': {
if (config.url_path) {
window.open(config.url_path);
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
break;
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
case 'navigate': {
if (config.navigation_path) {
history.pushState(null, '', config.navigation_path);
this.fireEvent(window, 'location-changed', {replace: false});
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
break;
2020-05-03 16:41:27 +02:00
}
}
}
}
2021-02-01 21:02:55 +01:00
toggleEntity(entityId) {
const turnOn = ["closed", "locked", "off"].includes(this._hass.states[entityId].state);
const stateDomain = entityId.split('.')[0];
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
let service;
switch (stateDomain) {
case "lock":
service = turnOn ? "unlock" : "lock";
break;
case "cover":
service = turnOn ? "open_cover" : "close_cover";
break;
default:
service = turnOn ? "turn_on" : "turn_off";
}
this._hass.callService(serviceDomain, service, {entity_id: entityId});
2020-05-03 16:41:27 +02:00
}
2021-02-01 21:02:55 +01:00
fireEvent(node, type, detail = {}, options = {}) {
2020-05-03 16:41:27 +02:00
const event = new Event(type, {
bubbles: options.bubbles || true,
cancelable: options.cancelable || true,
composed: options.composed || true,
});
2021-02-01 21:02:55 +01:00
event.detail = detail;
node.dispatchEvent(event);
2020-05-03 16:41:27 +02:00
}
forwardHaptic(type) {
2021-02-01 21:02:55 +01:00
const event = new Event('haptic', {bubbles: true, cancelable: false, composed: true});
2020-05-03 16:41:27 +02:00
event.detail = type;
this.dispatchEvent(event);
}
2021-02-01 21:02:55 +01:00
secondsToDuration(sec) {
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor((sec % 3600) % 60);
const leftPad = (num) => (num < 10 ? `0${num}` : num);
if (h > 0) return `${h}:${leftPad(m)}:${leftPad(s)}`;
if (m > 0) return `${m}:${leftPad(s)}`;
if (s > 0) return `${s}`;
return null;
}
2020-05-03 16:41:27 +02:00
}
customElements.define('multiple-entity-row', MultipleEntityRow);
2021-02-01 21:02:55 +01:00
})(window.LitElement || Object.getPrototypeOf(customElements.get('hui-masonry-view') || customElements.get('hui-view')));