Skip to content

Instantly share code, notes, and snippets.

@savikko
Last active May 31, 2022 20:59
Show Gist options
  • Save savikko/47b042ba8e2a6629dce3d4b154176fc9 to your computer and use it in GitHub Desktop.
Save savikko/47b042ba8e2a6629dce3d4b154176fc9 to your computer and use it in GitHub Desktop.
Home Assistant Stopwatch (draft)

Installation instructions

  1. Add stopwatch.js to your www/ folder
  2. Add two helpers, input_datetime and input_number, like this:
input_datetime:
  stopwatch:
    name: Stopwatch current
    has_date: true
    has_time: true

input_boolean:

input_number:
  stopwatch:
    name: Stopwatch save
    initial: 0
    min: 0
    max: 2147483647
    step: 1
  1. Restart HA
  2. Add stopwatch.js as lovelace resource (upper right corner, resources and add as local/stopwatch.js)
  3. Add new card:
type: custom:stop-watch
entity_datetime: input_datetime.stopwatch
entity_number: input_number.stopwatch

Todo:

  • Buttons should be icons
  • Show only relevant buttons
  • Probably pausing will not pause on another clients

How this works (or should work)

There should be some kind of state machine, but current implementation is as follows:

  • Stopwatch start time is saved to input_datetime which is defined on card config
  • If timestamp on sensor is 1 or less, it is considered to be null and 00:00:00 is shown
  • If timestamp is something else, card will show difference to that time (like increasing stopwatch)
  • When user pauses the stopwatch, it saves elapsed time to input_number sensor
  • If user wants to continue stopwatch, it saves new value to input_datetime: the current time minus elapsed
import {
LitElement,
html,
css,
} from "https://unpkg.com/lit-element@2.0.1/lit-element.js?module"; // i dont like the idea that something is downloaded from somewhere
class StopWatchCard extends LitElement {
static get properties() {
return {
hass: {},
config: {},
elapsedTime: "", // TODO: is this really needed? probably not, formatting should happen on render level
elapsed_timestamp: 0,
};
}
connectedCallback() {
// no idea what this actually does, but copypasting from internet is the way to go
super.connectedCallback();
console.log("this is now2");
this.interval = window.setInterval(() => {
const entityId = this.config.entity_datetime;
const state = this.hass.states[entityId];
const epoch = state.attributes.timestamp;
if (epoch > 1) {
const start = new Date(epoch * 1000);
const now = new Date();
const difference = now - start;
this.elapsed_timestamp = difference;
const time_str = this.timeToString(difference);
this.elapsedTime = time_str;
} else {
this.elapsedTime = "00:00:00";
}
}, 100);
}
disconnectedCallback() {
super.disconnectedCallback();
window.clearInterval(this.interval);
}
render() {
return html`
<ha-card>
<div
class="card-content"
style="display: flex; justify-content: center;"
>
<div class="button-content" style="padding: 16px;">
<button
@click="${(ev) => this.play()}"
icon="mdi:play-circle-outline"
>
PLAY
</button>
<button
@click="${(ev) => this.pause()}"
icon="mdi:play-circle-outline"
>
PAUSE
</button>
<button
@click="${(ev) => this.continue()}"
icon="mdi:play-circle-outline"
>
CONTINUE
</button>
<button
@click="${(ev) => this.reset()}"
icon="mdi:play-circle-outline"
>
RESET
</button>
</div>
<div
class="time-content"
style="font-size: 4rem; padding: 16px; text-align: center;"
>
${this.elapsedTime}
</div>
</div>
</ha-card>
`;
}
timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let diffInMs = (diffInSec - ss) * 100;
let ms = Math.floor(diffInMs);
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString().padStart(2, "0");
return `${formattedMM}:${formattedSS}:${formattedMS}`;
}
play() {
console.log("play");
const entityId = this.config.entity_datetime;
const now = Date.now() / 1000;
this.setStartTime(entityId, now);
this.disconnectedCallback();
this.connectedCallback();
}
reset() {
console.log("reset");
const entityId = this.config.entity_datetime;
this.setStartTime(entityId, 1);
this.connectedCallback();
}
pause() {
console.log("pause");
const entityId = this.config.entity_number;
const timeToSet = this.elapsed_timestamp;
this.setSaveTime(entityId, timeToSet);
this.disconnectedCallback();
}
continue() {
console.log("continue");
const { entity_number, entity_datetime } = this.config;
const timeToContinueFrom = +this.hass.states[entity_number].state;
console.log("time to continue from", timeToContinueFrom);
const now = Date.now() / 1000;
const timeToSet = parseInt(now - timeToContinueFrom / 1000);
console.log("now", now);
console.log("set", timeToSet);
this.setStartTime(entity_datetime, timeToSet);
this.connectedCallback();
}
setConfig(config) {
if (!config.entity_datetime) {
throw new Error(
"You need to define an input_datetime entity (entity_datetime)"
);
}
if (!config.entity_number) {
throw new Error(
"You need to define an input_number entity (entity_number)"
);
}
this.config = config;
}
getCardSize() {
return 1;
}
setStartTime(entity, time) {
console.log("setting time to", time);
this.hass.callService("input_datetime", "set_datetime", {
entity_id: entity,
timestamp: time,
});
}
setSaveTime(entity, value) {
console.log("setting number to", value);
this.hass.callService("input_number", "set_value", {
entity_id: entity,
value: value,
});
}
// TODO: all styles should be here
static get styles() {
return css`
ha-icon-button {
width: 64px;
height: 64px;
cursor: pointer;
--mdc-icon-size: 100%;
}
`;
}
}
customElements.define("stop-watch", StopWatchCard);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment