diff --git a/src/panels/lovelace/cards/hui-sensor-card.js b/src/panels/lovelace/cards/hui-sensor-card.js
deleted file mode 100755
index 58a8c96574..0000000000
--- a/src/panels/lovelace/cards/hui-sensor-card.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import { LitElement, html, svg } from "@polymer/lit-element";
-
-import "../../../components/ha-card";
-import "../../../components/ha-icon";
-
-import computeStateName from "../../../common/entity/compute_state_name";
-import stateIcon from "../../../common/entity/state_icon";
-
-import EventsMixin from "../../../mixins/events-mixin";
-
-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,
- _min: Number,
- _max: Number,
- };
- }
-
- setConfig(config) {
- if (!config.entity || config.entity.split(".")[0] !== "sensor") {
- throw new Error("Specify an entity from within the sensor domain.");
- }
-
- const cardConfig = {
- detail: 1,
- icon: false,
- hours_to_show: 24,
- ...config,
- };
- cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
- cardConfig.height = Number(cardConfig.height);
- cardConfig.detail =
- cardConfig.detail === 1 || cardConfig.detail === 2
- ? cardConfig.detail
- : 1;
-
- this._config = cardConfig;
- }
-
- shouldUpdate(changedProps) {
- const change = changedProps.has("_entity") || changedProps.has("_line");
- return change;
- }
-
- render({ _entity, _line } = this) {
- return html`
- ${this._style()}
-
-
-
- ${_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;
- }
-
- _coordinates(history, hours, width, detail = 1) {
- history = history.filter((item) => !Number.isNaN(Number(item.state)));
- this._min = Math.min.apply(Math, history.map((item) => Number(item.state)));
- this._max = Math.max.apply(Math, history.map((item) => Number(item.state)));
- const now = new Date().getTime();
-
- const reduce = (res, item, min = false) => {
- const age = now - new Date(item.last_changed).getTime();
- let key = Math.abs(age / (1000 * 3600) - hours);
- if (min) {
- key = (key - Math.floor(key)) * 60;
- key = (Math.round(key / 10) * 10).toString()[0];
- } else {
- key = Math.floor(key);
- }
- if (!res[key]) res[key] = [];
- res[key].push(item);
- return res;
- };
- history = history.reduce((res, item) => reduce(res, item), []);
- if (detail > 1) {
- history = history.map((entry) =>
- entry.reduce((res, item) => reduce(res, item, true), [])
- );
- }
- return this._calcPoints(history, hours, width, detail);
- }
-
- _calcPoints(history, hours, width, detail = 1) {
- const coords = [];
- const margin = 5;
- const height = 80;
- width -= margin * 2;
- let yRatio = (this._max - this._min) / height;
- yRatio = yRatio !== 0 ? yRatio : height;
- let xRatio = width / (hours - (detail === 1 ? 1 : 0));
- xRatio = isFinite(xRatio) ? xRatio : width;
- const getCoords = (item, i, offset = 0, depth = 1) => {
- if (depth > 1)
- return item.forEach((subItem, index) =>
- getCoords(subItem, i, index, depth - 1)
- );
- const average =
- item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
- item.length;
-
- const x = xRatio * (i + offset / 6) + margin;
- const y = height - (average - this._min) / yRatio + margin * 2;
- return coords.push([x, y]);
- };
-
- history.forEach((item, i) => getCoords(item, i, 0, detail));
- if (coords.length === 1) coords[1] = [width + margin, coords[0][1]];
- coords.push([width + margin, coords[coords.length - 1][1]]);
- return coords;
- }
-
- _getPath(coords) {
- let next;
- let Z;
- const X = 0;
- const Y = 1;
- let path = "";
- let last = coords.filter(Boolean)[0];
-
- path += `M ${last[X]},${last[Y]}`;
-
- for (let i = 0; i < coords.length; i++) {
- next = coords[i];
- Z = this._midPoint(last[X], last[Y], next[X], next[Y]);
- path += ` ${Z[X]},${Z[Y]}`;
- path += ` Q${next[X]},${next[Y]}`;
- last = next;
- }
-
- path += ` ${next[X]},${next[Y]}`;
- 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
- );
-
- if (stateHistory[0].length < 1) return;
- const coords = this._coordinates(
- stateHistory[0],
- this._config.hours_to_show,
- 500,
- this._config.detail
- );
- this._line = this._getPath(coords);
- }
-
- 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/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts
new file mode 100755
index 0000000000..86b2159082
--- /dev/null
+++ b/src/panels/lovelace/cards/hui-sensor-card.ts
@@ -0,0 +1,404 @@
+import {
+ html,
+ svg,
+ LitElement,
+ PropertyDeclarations,
+ PropertyValues,
+} from "@polymer/lit-element";
+import { TemplateResult } from "lit-html";
+import "@polymer/paper-spinner/paper-spinner";
+
+import { LovelaceCard } from "../types";
+import { LovelaceCardConfig } from "../../../data/lovelace";
+import { HomeAssistant } from "../../../types";
+import { fireEvent } from "../../../common/dom/fire_event";
+
+import computeStateName from "../../../common/entity/compute_state_name";
+import stateIcon from "../../../common/entity/state_icon";
+
+import "../../../components/ha-card";
+import "../../../components/ha-icon";
+import { fetchRecent } from "../../../data/history";
+
+const midPoint = (
+ _Ax: number,
+ _Ay: number,
+ _Bx: number,
+ _By: number
+): number[] => {
+ const _Zx = (_Ax - _Bx) / 2 + _Bx;
+ const _Zy = (_Ay - _By) / 2 + _By;
+ return [_Zx, _Zy];
+};
+
+const getPath = (coords: number[][]): string => {
+ let next;
+ let Z;
+ const X = 0;
+ const Y = 1;
+ let path = "";
+ let last = coords.filter(Boolean)[0];
+
+ path += `M ${last[X]},${last[Y]}`;
+
+ for (const coord of coords) {
+ next = coord;
+ Z = midPoint(last[X], last[Y], next[X], next[Y]);
+ path += ` ${Z[X]},${Z[Y]}`;
+ path += ` Q${next[X]},${next[Y]}`;
+ last = next;
+ }
+
+ path += ` ${next[X]},${next[Y]}`;
+ return path;
+};
+
+const calcPoints = (
+ history: any,
+ hours: number,
+ width: number,
+ detail: number,
+ min: number,
+ max: number
+): number[][] => {
+ const coords = [] as number[][];
+ const margin = 5;
+ const height = 80;
+ width -= 10;
+ let yRatio = (max - min) / height;
+ yRatio = yRatio !== 0 ? yRatio : height;
+ let xRatio = width / (hours - (detail === 1 ? 1 : 0));
+ xRatio = isFinite(xRatio) ? xRatio : width;
+ const getCoords = (item, i, offset = 0, depth = 1) => {
+ if (depth > 1) {
+ return item.forEach((subItem, index) =>
+ getCoords(subItem, i, index, depth - 1)
+ );
+ }
+ const average =
+ item.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
+ item.length;
+
+ const x = xRatio * (i + offset / 6) + margin;
+ const y = height - (average - min) / yRatio + margin * 2;
+ return coords.push([x, y]);
+ };
+
+ history.forEach((item, i) => getCoords(item, i, 0, detail));
+ if (coords.length === 1) {
+ coords[1] = [width + margin, coords[0][1]];
+ }
+
+ coords.push([width + margin, coords[coords.length - 1][1]]);
+ return coords;
+};
+
+const coordinates = (
+ history: any,
+ hours: number,
+ width: number,
+ detail: number
+): number[][] => {
+ history.forEach((item) => (item.state = Number(item.state)));
+ history = history.filter((item) => !Number.isNaN(item.state));
+
+ const min = Math.min.apply(Math, history.map((item) => item.state));
+ const max = Math.max.apply(Math, history.map((item) => item.state));
+ const now = new Date().getTime();
+
+ const reduce = (res, item, point) => {
+ const age = now - new Date(item.last_changed).getTime();
+
+ let key = Math.abs(age / (1000 * 3600) - hours);
+ if (point) {
+ key = (key - Math.floor(key)) * 60;
+ key = Number((Math.round(key / 10) * 10).toString()[0]);
+ } else {
+ key = Math.floor(key);
+ }
+ if (!res[key]) {
+ res[key] = [];
+ }
+ res[key].push(item);
+ return res;
+ };
+
+ history = history.reduce((res, item) => reduce(res, item, false), []);
+ if (detail > 1) {
+ history = history.map((entry) =>
+ entry.reduce((res, item) => reduce(res, item, true), [])
+ );
+ }
+ return calcPoints(history, hours, width, detail, min, max);
+};
+
+interface Config extends LovelaceCardConfig {
+ entity: string;
+ name?: string;
+ icon?: string;
+ graph?: string;
+ unit?: string;
+ detail?: number;
+ hours_to_show?: number;
+}
+
+class HuiSensorCard extends LitElement implements LovelaceCard {
+ public hass?: HomeAssistant;
+ private _config?: Config;
+ private _history?: any;
+ private _date?: Date;
+
+ static get properties(): PropertyDeclarations {
+ return {
+ hass: {},
+ _config: {},
+ _history: {},
+ };
+ }
+
+ public setConfig(config: Config): void {
+ if (!config.entity || config.entity.split(".")[0] !== "sensor") {
+ throw new Error("Specify an entity from within the sensor domain.");
+ }
+
+ const cardConfig = {
+ detail: 1,
+ hours_to_show: 24,
+ ...config,
+ };
+
+ cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
+ cardConfig.detail =
+ cardConfig.detail === 1 || cardConfig.detail === 2
+ ? cardConfig.detail
+ : 1;
+
+ this._config = cardConfig;
+ }
+
+ public getCardSize(): number {
+ return 3;
+ }
+
+ protected render(): TemplateResult {
+ if (!this._config || !this.hass) {
+ return html``;
+ }
+
+ const stateObj = this.hass.states[this._config.entity];
+
+ let graph;
+
+ if (this._config.graph === "line") {
+ if (!stateObj.attributes.unit_of_measurement) {
+ graph = html`
+
+ Entity: ${this._config.entity} - Has no Unit of Measurement and
+ therefore can not display a line graph.
+
+ `;
+ } else if (!this._history) {
+ graph = svg`
+
+ `;
+ } else {
+ graph = svg`
+
+ `;
+ }
+ } else {
+ graph = "";
+ }
+ return html`
+ ${this.renderStyle()}
+
+ ${
+ !stateObj
+ ? html`
+
+ Entity not available: ${this._config.entity}
+
+ `
+ : html`
+
+
+ ${stateObj.state}
+ ${
+ this._config.unit ||
+ stateObj.attributes.unit_of_measurement
+ }
+
+
+ `
+ }
+
+ `;
+ }
+
+ protected firstUpdated(): void {
+ this._date = new Date();
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ if (this._config && this._config.graph !== "line") {
+ return;
+ }
+
+ const minute = 60000;
+ if (changedProps.has("_config")) {
+ this._getHistory();
+ } else if (Date.now() - this._date!.getTime() >= minute) {
+ this._getHistory();
+ }
+ }
+
+ private _handleClick(): void {
+ fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
+ }
+
+ private async _getHistory(): Promise {
+ const endTime = new Date();
+ const startTime = new Date();
+ startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
+
+ const stateHistory = await fetchRecent(
+ this.hass,
+ this._config!.entity,
+ startTime,
+ endTime
+ );
+
+ if (stateHistory[0].length < 1) {
+ return;
+ }
+
+ const coords = coordinates(
+ stateHistory[0],
+ this._config!.hours_to_show!,
+ 500,
+ this._config!.detail!
+ );
+
+ this._history = getPath(coords);
+ this._date = new Date();
+ }
+
+ private renderStyle(): TemplateResult {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-sensor-card": HuiSensorCard;
+ }
+}
+
+customElements.define("hui-sensor-card", HuiSensorCard);