Created
September 3, 2024 20:18
-
-
Save sam2332/96b2b6e66b6250b8401880c353bbc982 to your computer and use it in GitHub Desktop.
HomeAssistant Lovelace js module for showing durations of states, useful for person trackers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class StateDurationCard extends HTMLElement { | |
set hass(hass) { | |
this._hass = hass; | |
if (!this.content) { | |
const cardTitle = this.config.title || 'State Duration Totals'; | |
this.innerHTML = ` | |
<ha-card> | |
<div class="card-header"> | |
<div class="name" style="cursor: pointer;">${cardTitle}</div> | |
</div> | |
<div class="card-content"></div> | |
</ha-card> | |
`; | |
this.content = this.querySelector('div.card-content'); | |
// Click event on the title to navigate to the history page with date range | |
const titleElement = this.querySelector('div.card-header div.name'); | |
titleElement.addEventListener('click', () => { | |
const hours = this.config.hours || 24; | |
const endDate = new Date(); | |
const startDate = new Date(endDate.getTime() - hours * 60 * 60 * 1000); | |
const startDateStr = startDate.toISOString().slice(0, 19); // 'YYYY-MM-DDTHH:MM:SS' | |
const endDateStr = endDate.toISOString().slice(0, 19); // 'YYYY-MM-DDTHH:MM:SS' | |
window.location.href = `/history?entity_id=${this.config.entity}&start_date=${startDateStr}&end_date=${endDateStr}`; | |
}); | |
this.updateContent(); | |
} | |
} | |
async updateContent() { | |
const entityId = this.config.entity; | |
const hours = this.config.hours || 24; // Default to the last 24 hours | |
const history = await this.fetchHistory(entityId, hours); | |
if (history.length > 0 && history[0].length > 0) { | |
const convertedEntries = history[0].map(state => ({ | |
...state, | |
last_changed: this.convertToLocalTime(state.last_changed) | |
})); | |
const totals = this.calculateTotals(convertedEntries); | |
// Convert the totals object to an array, sort by duration descending | |
const sortedTotals = Object.entries(totals).sort((a, b) => b[1] - a[1]); | |
let contentHTML = '<table width="100%">'; | |
sortedTotals.forEach(([state, duration]) => { | |
const formattedDuration = this.formatDuration(duration); | |
contentHTML += `<tr><td>${state}</td><td>${formattedDuration}</td></tr>`; | |
}); | |
contentHTML += '</table>'; | |
this.content.innerHTML = contentHTML; | |
} else { | |
this.content.innerHTML = '<p>No data available for the selected period.</p>'; | |
} | |
} | |
calculateTotals(states) { | |
const totals = {}; | |
const minDuration = this.config.min_duration_seconds * 1000; // Convert seconds to milliseconds | |
for (let i = 0; i < states.length; i++) { | |
const current = states[i]; | |
const next = states[i + 1]; | |
let duration = 0; | |
if (next) { | |
duration = next.last_changed - current.last_changed; | |
} else { | |
duration = new Date() - current.last_changed; // Until now | |
} | |
if (duration >= minDuration) { // Only add durations above the minimum | |
if (!totals[current.state]) { | |
totals[current.state] = 0; | |
} | |
totals[current.state] += duration; | |
} | |
} | |
return totals; | |
} | |
formatDuration(ms) { | |
const hours = Math.floor(ms / (1000 * 60 * 60)); | |
const minutes = Math.floor((ms / (1000 * 60)) % 60); | |
const seconds = Math.floor((ms / 1000) % 60); | |
let duration = ''; | |
if (hours > 0) { | |
duration += `${hours} hours, `; | |
} | |
if (minutes > 0) { | |
duration += `${minutes} minutes, `; | |
} | |
if (seconds > 0) { | |
duration += `${seconds} seconds`; | |
} | |
return duration.trim().replace(/,\s*$/, ''); // Remove trailing comma if present | |
} | |
convertToLocalTime(utcDatetime) { | |
return new Date(utcDatetime); | |
} | |
async fetchHistory(entityId, hours) { | |
const endDate = new Date(); | |
const startDate = new Date(endDate.getTime() - (hours * 60 * 60 * 1000)); | |
const url = `history/period/${startDate.toISOString()}?filter_entity_id=${entityId}`; | |
const response = await this._hass.callApi('GET', url); | |
return response; | |
} | |
setConfig(config) { | |
if (!config.entity) { | |
throw new Error('You need to define an entity'); | |
} | |
this.config = { | |
min_duration_seconds: 0, // Default value, showing all entries | |
...config | |
}; | |
} | |
getCardSize() { | |
return 3; | |
} | |
} | |
customElements.define('state-duration-card', StateDurationCard); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment