For anyone who wants to browse through footage captured by a security camera on a Lovelace dashboard, this Camera History plugin might be helpful. It will group the images by day and display the last image taken per day below each other. To browse through the images from a single day, simply drag the slider. Clicking on an image will show it in a new browser window.
Screenshot
Prerequisites
- Store images from your camera into a folder under
/config/www/
to expose it to the front end. - The filename should contain the date and time as two different integer values, for example:
Kitchen_20210508_184321.jpg
Cam_20210508-184321.jpg
Garage 20210508 184321 porch.jpg
- Use a folder sensor and point it at the directory.
Installation
- Save the below script as
/config/www/plugins/camera-history/camera-history.js
class CameraHistory extends HTMLElement {
set hass(hass) {
// Capture data
const entityId = this.config.entity;
const state = hass.states[entityId];
var files = state ? state.attributes.file_list : ["none"];
// Initialize
if (!this.content) {
// Add card
const card = document.createElement("ha-card");
card.header = (this.config.title || "");
this.content = document.createElement("div");
this.content.style.padding = "0 16px 16px";
card.appendChild(this.content);
this.appendChild(card);
// Expose hass methods
this.localize = hass.localize;
}
// Set up file names for change detection
files = files.map(file => file.replace("/config/www/", "/local/"));
files.sort();
files.reverse();
files = files.filter(filename => !filename.includes("latest"));
// Change detection
if (!this.previousFiles || JSON.stringify(this.previousFiles) != JSON.stringify(files)) {
this.files = files;
this.previousFiles = files;
this.draw();
}
}
draw() {
var dates = this.files.reduce(function(output, current)
{
var date = current.match(/[0-9]{8}/);
if (!date)
{
date = "00000000";
}
if (!output[date[0]])
{
output[date[0]] = [];
}
output[date[0]].push(current);
return output;
}, []);
dates.reverse();
var output = `
<style>
.imagecontainer {
margin: 0;
padding: 0;
}
.slidecontainer {
margin: 0;
padding: 0;
width: 100%; /* Width of the outside container */
}
/* The slider itself */
input[data-slider] {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 40px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
margin: 0;
}
/* Mouse-over effects */
input[data-slider]:hover {
opacity: 0.8; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
input[data-slider]::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 40px; /* Set a specific slider handle width */
height: 40px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
input[data-slider]::-moz-range-thumb {
width: 25px; /* Set a specific slider handle width */
height: 25px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
h5[data-timestamp] {
float: left;
margin-top: -35px;
margin-left: 10px;
}
h5[data-frame] {
float: right;
margin-top: -35px;
margin-right: 10px;
}
</style>
<table>
${Object.keys(dates).map(key =>
{
var filename = dates[key][0];
var timestamp = filename.match(/(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})(.*)(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})/);
var date = new Date(`${timestamp.groups.year}-${timestamp.groups.month}-${timestamp.groups.day} ${timestamp.groups.hour}:${timestamp.groups.minute}:${timestamp.groups.second}`);
return `
<tr>
<td>
<div class="imagecontainer">
<a href="${filename}" data-image="${key}" target="_blank">
<img src="${filename}" data-image="${key}" width="100%">
</a>
</div>
<div class="slidecontainer">
<input type="range" min="0" max="${dates[key].length - 1}" value="0" data-slider data-image="${key}">
<h5 class="date" data-timestamp="${key}">${this.formatTimestamp(date, timestamp)}</h5>
<h5 class="index" data-frame="${key}">1 / ${dates[key].length}</h5>
</div>
</td>
<!--
<td width="80">
<center>
<ha-icon icon="mdi:clock"></ha-icon><br/>
<strong>${this.formatDay(date)}</strong><br/>
<span>${timestamp.groups.year}-${timestamp.groups.month}-${timestamp.groups.day}</span><br/>
</center>
</td>
-->
</tr>`
}).join("")}
</table>`;
this.content.innerHTML = output;
Array.from(this.content.querySelectorAll("input[data-slider]")).map(slider =>
{
slider.addEventListener("input", e => {
// Process selection
var currentImage = e.target.value;
var filename = dates[e.target.dataset.image][currentImage];
var timestamp = filename.match(/(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})_(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})/);
var date = new Date(`${timestamp.groups.year}-${timestamp.groups.month}-${timestamp.groups.day} ${timestamp.groups.hour}:${timestamp.groups.minute}:${timestamp.groups.second}`);
// Update image
this.content.querySelector(`img[data-image="${e.target.dataset.image}"]`).src = filename;
// Update link
this.content.querySelector(`a[data-image="${e.target.dataset.image}"]`).href = filename;
// Update timestamp
this.content.querySelector(`h5[data-timestamp="${e.target.dataset.image}"]`).innerHTML = this.formatTimestamp(date, timestamp);
this.content.querySelector(`h5[data-frame="${e.target.dataset.image}"]`).innerText = `${parseInt(currentImage) + 1} / ${dates[e.target.dataset.image].length}`;
})
});
}
setConfig(config) {
if (!config.entity) {
throw new Error("You need to define an entity");
}
this.config = config;
}
// The height of your card. Home Assistant uses this to automatically
// distribute all cards over the available columns.
getCardSize() {
return 5;
}
formatDay(timestamp) {
if (timestamp === null || timestamp === undefined) {
return "";
}
// Calculate time passed
var today = new Date((new Date()).getFullYear(), (new Date()).getMonth(), (new Date()).getDate());
var date = new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate());
var daysBetween = Math.round((today.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
// Output date string
if (daysBetween == 0) {
return this.localize("ui.components.calendar.today");
/*
} else if (daysBetween == 1) {
return "Yesterday";
} else if (daysBetween < 7) {
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()];
} else if (daysBetween < 14) {
return `Last week ${["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getDay()]}`;
*/
} else {
return this.localize("ui.duration.day", "count", daysBetween);
}
}
formatTimestamp(date, timestamp) {
return `${timestamp.groups.year}-${timestamp.groups.month}-${timestamp.groups.day} (${this.formatDay(date)}) · ${timestamp.groups.hour}:${timestamp.groups.minute}:${timestamp.groups.second}`;
}
}
customElements.define("camera-history", CameraHistory);
- Go to Configuration | Lovelace Dashboards | Resources
- Click Add Resource
- Set URL to
/local/plugins/camera-history/camera-history.js
- Set Resource type to JavaScript Module
- Click Update
- Edit your dashboard and add a new Manual Card
- Add a card configuration similar to:
type: 'custom:camera-history'
entity: sensor.kitchenFolder
title: Kitchen
Possible enhancements
- Defining in the card configuration how many days to display.
- Move the date from the right of the image, into the slider.
- Add a dedicated popup when clicking on an image.
- Better wording on the dates (so âyesterdayâ, âlast Wednesdayâ, âTuesday last weekâ) instead of the simple â1 day agoâ.