mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Add sensor-graph-card (#1744)
* Added sensor-graph-card * Removed Object as type * Removed unused attributes * Fixed card config * Changed svg rendering to lit html svg * Fixed config conversion * Changed to _config, _entity, _line as private * Removed lit-element package * Renamed to hui-sensor-card * lit-html 0.6.2 changes * Added logic for graph config option
This commit is contained in:
parent
af2cb1be1a
commit
ea0b5d5e26
280
src/panels/lovelace/cards/hui-sensor-card.js
Normal file
280
src/panels/lovelace/cards/hui-sensor-card.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import { LitElement, html, svg } from '@polymer/lit-element';
|
||||||
|
|
||||||
|
import '../../../components/ha-card.js';
|
||||||
|
import '../../../components/ha-icon.js';
|
||||||
|
|
||||||
|
import computeStateName from '../../../common/entity/compute_state_name.js';
|
||||||
|
import stateIcon from '../../../common/entity/state_icon.js';
|
||||||
|
|
||||||
|
import EventsMixin from '../../../mixins/events-mixin.js';
|
||||||
|
|
||||||
|
class HuiSensorCard extends EventsMixin(LitElement) {
|
||||||
|
set hass(hass) {
|
||||||
|
this._hass = hass;
|
||||||
|
const entity = hass.states[this._config.entity];
|
||||||
|
if (entity && this._entity !== entity) {
|
||||||
|
this._entity = entity;
|
||||||
|
if (this._config.graph !== 'none' && entity.attributes.unit_of_measurement) {
|
||||||
|
this._getHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
_hass: { },
|
||||||
|
_config: { },
|
||||||
|
_entity: { },
|
||||||
|
_line: String
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
if (!config.entity || config.entity.split('.')[0] !== 'sensor') {
|
||||||
|
throw new Error('Specify an entity from within the sensor domain.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardConfig = Object.assign({
|
||||||
|
icon: false,
|
||||||
|
hours_to_show: 24,
|
||||||
|
accuracy: 10,
|
||||||
|
height: 100,
|
||||||
|
line_width: 5,
|
||||||
|
line_color: 'var(--accent-color)'
|
||||||
|
}, config);
|
||||||
|
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
|
||||||
|
cardConfig.accuracy = Number(cardConfig.accuracy);
|
||||||
|
cardConfig.height = Number(cardConfig.height);
|
||||||
|
cardConfig.line_width = Number(cardConfig.line_width);
|
||||||
|
|
||||||
|
this._config = cardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldUpdate(changedProps) {
|
||||||
|
const change = (
|
||||||
|
changedProps.has('_entity')
|
||||||
|
|| changedProps.has('_line')
|
||||||
|
);
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ _config, _entity, _line } = this) {
|
||||||
|
return html`
|
||||||
|
${this._style()}
|
||||||
|
<ha-card @click=${this._handleClick}>
|
||||||
|
<div class='flex'>
|
||||||
|
<div class='icon'>
|
||||||
|
<ha-icon .icon=${this._computeIcon(_entity)}></ha-icon>
|
||||||
|
</div>
|
||||||
|
<div class='header'>
|
||||||
|
<span class='name'>${this._computeName(_entity)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex info'>
|
||||||
|
<span id='value'>${_entity.state}</span>
|
||||||
|
<span id='measurement'>${this._computeUom(_entity)}</span>
|
||||||
|
</div>
|
||||||
|
<div class='graph'>
|
||||||
|
<div>
|
||||||
|
${_line ? svg`
|
||||||
|
<svg width='100%' height='100%' viewBox='0 0 500 ${_config.height}'>
|
||||||
|
<path d=${_line} fill='none' stroke=${_config.line_color}
|
||||||
|
stroke-width=${_config.line_width}
|
||||||
|
stroke-linecap='round' stroke-linejoin='round' />
|
||||||
|
</svg>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleClick() {
|
||||||
|
this.fire('hass-more-info', { entityId: this._config.entity });
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeIcon(item) {
|
||||||
|
return this._config.icon || stateIcon(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeName(item) {
|
||||||
|
return this._config.name || computeStateName(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeUom(item) {
|
||||||
|
return this._config.unit || item.attributes.unit_of_measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGraph(items, width, height) {
|
||||||
|
const values = this._getValueArr(items);
|
||||||
|
const coords = this._calcCoordinates(values, width, height);
|
||||||
|
return this._getPath(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getValueArr(items) {
|
||||||
|
return items.map(item => Number(item.state) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcCoordinates(values, width, height) {
|
||||||
|
const margin = this._config.line_width;
|
||||||
|
width -= margin * 2;
|
||||||
|
height -= margin * 2;
|
||||||
|
const min = Math.floor(Math.min.apply(null, values) * 0.95);
|
||||||
|
const max = Math.ceil(Math.max.apply(null, values) * 1.05);
|
||||||
|
|
||||||
|
const yRatio = (max - min) / height;
|
||||||
|
const xRatio = width / (values.length - 1);
|
||||||
|
|
||||||
|
return values.map((value, i) => {
|
||||||
|
const y = height - ((value - min) / yRatio) || 0;
|
||||||
|
const x = (xRatio * i) + margin;
|
||||||
|
return [x, y];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPath(points) {
|
||||||
|
const SPACE = ' ';
|
||||||
|
let next; let Z;
|
||||||
|
const X = 0;
|
||||||
|
const Y = 1;
|
||||||
|
let path = '';
|
||||||
|
let point = points[0];
|
||||||
|
|
||||||
|
path += 'M' + point[X] + ',' + point[Y];
|
||||||
|
const first = point;
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
next = points[i];
|
||||||
|
Z = this._midPoint(point[X], point[Y], next[X], next[Y]);
|
||||||
|
path += SPACE + Z[X] + ',' + Z[Y];
|
||||||
|
path += 'Q' + Math.floor(next[X]) + ',' + next[Y];
|
||||||
|
point = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
const second = points[1];
|
||||||
|
Z = this._midPoint(first[X], first[Y], second[X], second[Y]);
|
||||||
|
path += SPACE + Math.floor(next[X]) + '.' + points[points.length - 1];
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
_midPoint(Ax, Ay, Bx, By) {
|
||||||
|
const Zx = (Ax - Bx) / 2 + Bx;
|
||||||
|
const Zy = (Ay - By) / 2 + By;
|
||||||
|
return [Zx, Zy];
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getHistory() {
|
||||||
|
const endTime = new Date();
|
||||||
|
const startTime = new Date();
|
||||||
|
startTime.setHours(endTime.getHours() - this._config.hours_to_show);
|
||||||
|
const stateHistory = await this._fetchRecent(this._config.entity, startTime, endTime);
|
||||||
|
const history = stateHistory[0];
|
||||||
|
const valArray = [history[history.length - 1]];
|
||||||
|
|
||||||
|
let pos = history.length - 1;
|
||||||
|
const accuracy = (this._config.accuracy) <= pos ? this._config.accuracy : pos;
|
||||||
|
let increment = Math.ceil(history.length / accuracy);
|
||||||
|
increment = (increment <= 0) ? 1 : increment;
|
||||||
|
for (let i = accuracy; i >= 2; i--) {
|
||||||
|
pos -= increment;
|
||||||
|
valArray.unshift(pos >= 0 ? history[pos] : history[0]);
|
||||||
|
}
|
||||||
|
this._line = this._getGraph(valArray, 500, this._config.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchRecent(entityId, startTime, endTime) {
|
||||||
|
let url = 'history/period';
|
||||||
|
if (startTime) url += '/' + startTime.toISOString();
|
||||||
|
url += '?filter_entity_id=' + entityId;
|
||||||
|
if (endTime) url += '&end_time=' + endTime.toISOString();
|
||||||
|
|
||||||
|
return await this._hass.callApi('GET', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardSize() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
_style() {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
opacity: .8;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
display: -webkit-box;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
max-height: 1.4rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
opacity: .8;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
color: var(--paper-item-icon-color, #44739e);
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 16px 0 16px 8px;
|
||||||
|
}
|
||||||
|
#value {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1em;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
#measurement {
|
||||||
|
align-self: flex-end;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.2em;
|
||||||
|
margin-top: .1em;
|
||||||
|
opacity: .6;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
.graph {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.graph > div {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin: auto 8px;
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('hui-sensor-card', HuiSensorCard);
|
@ -16,6 +16,7 @@ import '../cards/hui-picture-elements-card';
|
|||||||
import '../cards/hui-picture-entity-card';
|
import '../cards/hui-picture-entity-card';
|
||||||
import '../cards/hui-picture-glance-card';
|
import '../cards/hui-picture-glance-card';
|
||||||
import '../cards/hui-plant-status-card.js';
|
import '../cards/hui-plant-status-card.js';
|
||||||
|
import '../cards/hui-sensor-card.js';
|
||||||
import '../cards/hui-vertical-stack-card.js';
|
import '../cards/hui-vertical-stack-card.js';
|
||||||
import '../cards/hui-weather-forecast-card';
|
import '../cards/hui-weather-forecast-card';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ const CARD_TYPES = new Set([
|
|||||||
'picture-entity',
|
'picture-entity',
|
||||||
'picture-glance',
|
'picture-glance',
|
||||||
'plant-status',
|
'plant-status',
|
||||||
|
'sensor',
|
||||||
'vertical-stack',
|
'vertical-stack',
|
||||||
'weather-forecast'
|
'weather-forecast'
|
||||||
]);
|
]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user