diff --git a/src/panels/lovelace/entity-rows/hui-timer-entity-row.js b/src/panels/lovelace/entity-rows/hui-timer-entity-row.js
deleted file mode 100644
index fbcfd143d5..0000000000
--- a/src/panels/lovelace/entity-rows/hui-timer-entity-row.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../components/hui-generic-entity-row";
-
-import timerTimeRemaining from "../../../common/entity/timer_time_remaining";
-import secondsToDuration from "../../../common/datetime/seconds_to_duration";
-
-class HuiTimerEntityRow extends PolymerElement {
- static get template() {
- return html`
-
- ${this.timerControlTemplate}
-
- `;
- }
-
- static get timerControlTemplate() {
- return html`
-
[[_computeDisplay(_stateObj, _timeRemaining)]]
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- _config: Object,
- _stateObj: {
- type: Object,
- computed: "_computeStateObj(hass.states, _config.entity)",
- observer: "_stateObjChanged",
- },
- _timeRemaining: Number,
- };
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- this._clearInterval();
- }
-
- _stateObjChanged(stateObj) {
- if (stateObj) {
- this._startInterval(stateObj);
- } else {
- this._clearInterval();
- }
- }
-
- _clearInterval() {
- if (this._updateRemaining) {
- clearInterval(this._updateRemaining);
- this._updateRemaining = null;
- }
- }
-
- _startInterval(stateObj) {
- this._clearInterval();
- this._calculateRemaining(stateObj);
-
- if (stateObj.state === "active") {
- this._updateRemaining = setInterval(
- () => this._calculateRemaining(this._stateObj),
- 1000
- );
- }
- }
-
- _calculateRemaining(stateObj) {
- this._timeRemaining = timerTimeRemaining(stateObj);
- }
-
- _computeDisplay(stateObj, time) {
- if (!stateObj) return null;
-
- if (stateObj.state === "idle" || time === 0) return stateObj.state;
-
- let display = secondsToDuration(time);
-
- if (stateObj.state === "paused") {
- display += " (paused)";
- }
-
- return display;
- }
-
- _computeStateObj(states, entityId) {
- return states && entityId in states ? states[entityId] : null;
- }
-
- setConfig(config) {
- if (!config || !config.entity) {
- throw new Error("Entity not configured.");
- }
- this._config = config;
- }
-}
-customElements.define("hui-timer-entity-row", HuiTimerEntityRow);
diff --git a/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts
new file mode 100644
index 0000000000..7e7383961d
--- /dev/null
+++ b/src/panels/lovelace/entity-rows/hui-timer-entity-row.ts
@@ -0,0 +1,128 @@
+import {
+ html,
+ LitElement,
+ TemplateResult,
+ property,
+ PropertyValues,
+} from "lit-element";
+
+import "../components/hui-generic-entity-row";
+import "../components/hui-warning";
+
+import timerTimeRemaining from "../../../common/entity/timer_time_remaining";
+import secondsToDuration from "../../../common/datetime/seconds_to_duration";
+import { HomeAssistant } from "../../../types";
+import { EntityConfig } from "./types";
+import { HassEntity } from "home-assistant-js-websocket";
+
+class HuiTimerEntityRow extends LitElement {
+ @property() public hass?: HomeAssistant;
+ @property() private _config?: EntityConfig;
+ @property() private _timeRemaining?: number;
+ private _interval?: number;
+
+ public setConfig(config: EntityConfig): void {
+ if (!config) {
+ throw new Error("Configuration error");
+ }
+ this._config = config;
+ }
+
+ public disconnectedCallback(): void {
+ super.disconnectedCallback();
+ this._clearInterval();
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._config || !this.hass) {
+ return html``;
+ }
+
+ const stateObj = this.hass.states[this._config.entity];
+
+ if (!stateObj) {
+ return html`
+ ${this.hass.localize(
+ "ui.panel.lovelace.warning.entity_not_found",
+ "entity",
+ this._config.entity
+ )}
+ `;
+ }
+
+ return html`
+
+ ${this._computeDisplay(stateObj)}
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+
+ if (changedProps.has("hass")) {
+ const stateObj = this.hass!.states[this._config!.entity];
+ const oldHass = changedProps.get("hass") as this["hass"];
+ const oldStateObj = oldHass
+ ? oldHass.states[this._config!.entity]
+ : undefined;
+
+ if (oldStateObj !== stateObj) {
+ this._startInterval(stateObj);
+ } else if (!stateObj) {
+ this._clearInterval();
+ }
+ }
+ }
+
+ private _clearInterval(): void {
+ if (this._interval) {
+ window.clearInterval(this._interval);
+ this._interval = undefined;
+ }
+ }
+
+ private _startInterval(stateObj: HassEntity): void {
+ this._clearInterval();
+ this._calculateRemaining(stateObj);
+
+ if (stateObj.state === "active") {
+ this._interval = window.setInterval(
+ () => this._calculateRemaining(stateObj),
+ 1000
+ );
+ }
+ }
+
+ private _calculateRemaining(stateObj: HassEntity): void {
+ this._timeRemaining = timerTimeRemaining(stateObj);
+ }
+
+ private _computeDisplay(stateObj: HassEntity): string | null {
+ if (!stateObj) {
+ return null;
+ }
+
+ if (stateObj.state === "idle" || this._timeRemaining === 0) {
+ return this.hass!.localize("state.timer." + stateObj.state);
+ }
+
+ let display = secondsToDuration(this._timeRemaining || 0);
+
+ if (stateObj.state === "paused") {
+ display += ` (${this.hass!.localize("state.timer.paused")})`;
+ }
+
+ return display;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-timer-entity-row": HuiTimerEntityRow;
+ }
+}
+
+customElements.define("hui-timer-entity-row", HuiTimerEntityRow);
diff --git a/src/translations/en.json b/src/translations/en.json
index 35207ed514..9797982c17 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -271,6 +271,11 @@
"off": "[%key:state::default::off%]",
"on": "[%key:state::default::on%]"
},
+ "timer": {
+ "active": "active",
+ "idle": "idle",
+ "paused": "paused"
+ },
"vacuum": {
"cleaning": "Cleaning",
"docked": "Docked",