diff --git a/src/panels/lovelace/cards/hui-sensor-card.js b/src/panels/lovelace/cards/hui-sensor-card.js new file mode 100644 index 0000000000..fe03fedd2d --- /dev/null +++ b/src/panels/lovelace/cards/hui-sensor-card.js @@ -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()} + +
+
+ +
+
+ ${this._computeName(_entity)} +
+
+
+ ${_entity.state} + ${this._computeUom(_entity)} +
+
+
+ ${_line ? svg` + + + ` : ''} +
+
+
`; + } + + _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` + `; + } +} + +customElements.define('hui-sensor-card', HuiSensorCard); diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js index f2648cb78f..0975aef47f 100644 --- a/src/panels/lovelace/common/create-card-element.js +++ b/src/panels/lovelace/common/create-card-element.js @@ -16,6 +16,7 @@ import '../cards/hui-picture-elements-card'; import '../cards/hui-picture-entity-card'; import '../cards/hui-picture-glance-card'; import '../cards/hui-plant-status-card.js'; +import '../cards/hui-sensor-card.js'; import '../cards/hui-vertical-stack-card.js'; import '../cards/hui-weather-forecast-card'; @@ -38,6 +39,7 @@ const CARD_TYPES = new Set([ 'picture-entity', 'picture-glance', 'plant-status', + 'sensor', 'vertical-stack', 'weather-forecast' ]);