So i edited this because now it doesnt have anything to do with a file explorer but solves my problem.
So i needed a solution for a simple nvr and a way to go back in history to check videos to see what happened and also see a live preview of what is happening live.
In the begining i was under the impression that having a file explorer would solve the problem but you can follow the topic to see why that was a bad ideea.
Now lets get back to the final solution that works for me:
This whole concept is using the Ipcamera binding so be sure to configure that and then we need some items here is an example:
//very important you need to group them for this to work
Group g_outside_front_camera_camera "Front door outside camera" <camera> (g_outside_front) ["Camera"]
//
Image g_outside_front_camera_camera_Image <camera> (g_outside_front_camera_camera) ["Status"] { channel="ipcamera:generic:camera_intercom:image" }
String g_outside_front_camera_camera_Image_URL <camera> (g_outside_front_camera_camera) ["Status"] { channel="ipcamera:generic:camera_intercom:imageUrl" }
String g_outside_front_camera_camera_MJPEG_URL <camera> (g_outside_front_camera_camera) ["Status"] { channel="ipcamera:generic:camera_intercom:mjpegUrl" }
String g_outside_front_camera_camera_files_list
Number g_outside_front_camera_camera_files_number
DateTime g_outside_front_camera_camera_datetime
Then we need to place this script inside your automation folder and make sure you change the variables inside(in the future i would like to integrate them inside the widget to be able to dynamicly set them). Also make sure you know where ipcamera folder recording is you can set it yourself inside the configuration thing. Another important thing is having an item that triggers the recording in my case its an contact item or what you want and modify the script based on that:
const Paths = Java.type('java.nio.file.Paths');
const Files = Java.type('java.nio.file.Files');
let recordingTimeoutId = null;
let isRecording = false;
let recordingDurationInSeconds;
// Adjustable Variables
const ruleName = "CameraIntercomRecordingRule"; // Common name for the rule and logs
let folderPathString = '/media/storage/camera_intercom'; // Folder path
let movementDetectionItem = "Front_house_detector_movement"; // Item for movement detection
//here change to whatever you want make sure that the number is corresponding to the time unit
let fileAgeForDeletion = 3; // Numeric value for time unit
let timeUnit = 'days'; // Can be 'minutes', 'hours', or 'days'
// end
//here change to whatever you want make sure that the number is corresponding to the time unit
let recordingDuration = 60; // Numeric value for duration
let recordingDurationUnit = 'seconds'; // Can be 'seconds', 'minutes', or 'hours'
//end
let baseItemName = "g_outside_front_camera_camera"; // Base item name
let cameraConfigString = "ipcamera:generic:camera_intercom"; // Camera configuration string
console.info(`${ruleName}: Initialization complete`);
// Function to update the list of last N files
function updateLastNFiles() {
const folderPath = Paths.get(folderPathString);
const currentTime = time.ZonedDateTime.now();
const currentTimeMillis = currentTime.toInstant().toEpochMilli();
let selectedDateTimeMillis, lowerBoundMillis, upperBoundMillis;
// Fetch and parse selected date-time
let selectedDateTimeString = items.getItem(baseItemName + '_datetime').state.toString();
console.info(`Debug: Selected Date Time String: ${selectedDateTimeString}`);
try {
let formatter = time.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
let selectedDateTime = time.ZonedDateTime.parse(selectedDateTimeString, formatter);
selectedDateTimeMillis = selectedDateTime.toInstant().toEpochMilli();
// Calculate the 30-minute window around the selected date-time
const thirtyMinutesInMillis = 30 * 60 * 1000; // 30 minutes in milliseconds
lowerBoundMillis = selectedDateTimeMillis - thirtyMinutesInMillis;
upperBoundMillis = selectedDateTimeMillis + thirtyMinutesInMillis;
} catch (e) {
console.error(`Error parsing date-time: ${e.message}`);
return; // exit the function if parsing fails
}
try {
let directoryStream = Files.newDirectoryStream(folderPath);
let iterator = directoryStream.iterator();
let files = [];
while (iterator.hasNext()) {
let path = iterator.next();
// Skip files that do not end with .mp4
if (!path.toString().endsWith('.mp4')) {
continue;
}
if (!Files.isWritable(path)) {
console.warn("No permission to delete the file: " + path);
continue;
}
let fileTime = Files.getLastModifiedTime(path).toInstant().toEpochMilli();
// Check for file age for deletion
let ageInMillis = currentTimeMillis - fileTime;
let ageInSelectedUnit;
switch (timeUnit) {
case 'minutes':
ageInSelectedUnit = ageInMillis / (1000 * 60);
break;
case 'hours':
ageInSelectedUnit = ageInMillis / (1000 * 60 * 60);
break;
case 'days':
ageInSelectedUnit = ageInMillis / (1000 * 60 * 60 * 24);
break;
default:
console.warn("Invalid time unit: " + timeUnit);
return;
}
if (ageInSelectedUnit > fileAgeForDeletion) {
Files.delete(path);
continue;
}
// Check if the file falls within the 30-minute window around the selected date-time
if (fileTime >= lowerBoundMillis && fileTime <= upperBoundMillis) {
files.push({ name: path.getFileName().toString(), time: fileTime });
}
}
// Sort the .mp4 files by time
files.sort((a, b) => b.time - a.time);
// Update the files list
const filteredFilesList = files.map(file => file.name).join(", ");
items.getItem(baseItemName + '_files_list').sendCommand(filteredFilesList);
items.getItem(baseItemName + '_files_number').sendCommand(files.length);
directoryStream.close();
} catch (e) {
console.error(`${ruleName}: An error occurred in updateLastNFiles: ${e.message}`);
}
}
// Function to start recording
function startRecording() {
try {
let formattedDate = time.ZonedDateTime.now().format(time.DateTimeFormatter.ofPattern('yyyy-MM-dd\'-\'HH:mm:ss'));
let ipcameraActions = actions.get("ipcamera", cameraConfigString);
switch (recordingDurationUnit) {
case 'seconds':
recordingDurationInSeconds = recordingDuration;
break;
case 'minutes':
recordingDurationInSeconds = recordingDuration * 60;
break;
case 'hours':
recordingDurationInSeconds = recordingDuration * 60 * 60;
break;
default:
console.warn("Invalid recording duration unit: " + recordingDurationUnit);
return;
}
ipcameraActions.recordMP4(formattedDate, recordingDurationInSeconds);
isRecording = true;
updateLastNFiles();
} catch (e) {
console.error(`${ruleName}: An error occurred while starting the recording: ${e.message}`);
}
}
// Function to handle timer
function recordingTimerFunc() {
try {
if (items.getItem(movementDetectionItem).state === "OPEN") {
if (!isRecording) {
startRecording();
}
if (recordingTimeoutId !== null) {
clearTimeout(recordingTimeoutId);
}
recordingTimeoutId = setTimeout(() => {
try {
isRecording = false;
recordingTimerFunc();
} catch (e) {
console.error(`${ruleName}: An error occurred during the timer's callback: ${e.message}`);
}
}, recordingDurationInSeconds * 1000);
} else {
if (recordingTimeoutId !== null) {
clearTimeout(recordingTimeoutId);
}
isRecording = false;
}
} catch (e) {
console.error(`${ruleName}: An error occurred in recordingTimerFunc: ${e.message}`);
}
}
rules.JSRule({
name: ruleName,
description: `Starts recording when an item changes to OPEN - ${ruleName}`,
triggers: [
triggers.ItemStateChangeTrigger(movementDetectionItem, "CLOSED", "OPEN")
],
execute: () => {
console.info(`${ruleName}: Rule triggered`);
if (!isRecording) {
console.info(`${ruleName}: Not currently recording, starting now...`);
startRecording();
// Set the recording timeout only when a new recording starts
recordingTimeoutId = setTimeout(() => {
console.info(`${ruleName}: Recording timeout reached. Stopping recording.`);
isRecording = false;
recordingTimerFunc();
}, recordingDurationInSeconds * 1000);
} else {
console.info(`${ruleName}: Already recording, skipping...`);
}
}
});
rules.JSRule({
name: ruleName + "DateTimeChange",
description: `Updates file list when the date-time item changes - ${ruleName}`,
triggers: [
triggers.ItemStateChangeTrigger(baseItemName + '_datetime')
],
execute: () => {
console.info(`${ruleName}: Date-time item changed, updating file list...`);
updateLastNFiles();
}
});
then we need the widget that you have to configure its parameters i think its self explanatory (this is based on the work of @matt1):
uid: CameraHistoryNVR
tags: []
props:
parameters:
- context: item
label: Select the Camera (Equipment - Group) "example: g_outside_front_camera_camera"
name: camera
required: true
type: TEXT
- description: Camera link "example: http://youripadress:8080/ipcamera/camera_intercom/"
label: Base URL
name: cameraBaseURL
required: true
type: TEXT
- label: Custom Label for card
name: customLabel
required: false
type: TEXT
- context: item
description: Date item used "example: g_outside_front_camera_camera_datetime"
label: Datetime item
name: item_date
required: false
type: TEXT
timestamp: Sep 8, 2023, 5:24:23 PM
component: f7-card
config:
title: =props.customLabel || 'Ip Camera'
key: "=(vars.selectedVideoName === undefined) ? Math.random() : Math.random() + vars.selectedVideoName"
style:
--f7-card-margin-horizontal: 0px
--f7-card-margin-vertical: 0px
border-radius: 6px
slots:
default:
- component: oh-video-card
config:
key: =vars.selectedVideoName || 'defaultKey'
hideControls: false
startManually: false
url: "=vars.showVideoPlayer ? (props.cameraBaseURL + (vars.selectedVideoName || 'ipcamera.m3u8')) : ''"
visible: =vars.showVideoPlayer || false
- component: oh-image-card
config:
lazy: true
lazyFadeIn: true
style:
border-radius: 6px
height: auto
margin: 0px
width: 100%
url: "=props.cameraBaseURL.endsWith('/') ? (props.cameraBaseURL + 'autofps.mjpeg') : (props.cameraBaseURL + '/autofps.mjpeg')"
visible: =!vars.showVideoPlayer
action: variable
actionVariable: showVideoPlayer
actionVariableValue: true
- component: f7-list
config:
accordionList: true
slots:
default:
- component: f7-list
config:
accordionList: true
slots:
default:
- component: f7-row
slots:
default:
- component: oh-button
config:
text: Play
action: variable
actionVariable: showVideoPlayer
actionVariableValue: true
- component: oh-button
config:
text: Stop
action: variable
actionVariable: showVideoPlayer
actionVariableValue: false
- component: oh-input-card
config:
outline: true
clearButton: false
inputmode: text
item: =props.item_date
placeholder: =items[props.item_date].state
title: Set timeframe for searching
type: datetime-local
sendButton: true
defaultValue: true
- component: f7-list-item
config:
accordionItem: true
title: ="Found "+ items[props.camera + '_files_number'].state + " files - Selected " + (vars.selectedVideoName || 'none')
slots:
default:
- component: f7-accordion-content
slots:
default:
- component: oh-repeater
config:
for: videoName
in: =items[props.camera + '_files_list'].state.split(', ').filter(file => file.endsWith('.mp4'))
fragment: true
slots:
default:
- component: oh-list-item
config:
title: =loop.videoName
action: variable
actionVariable: selectedVideoName
actionVariableValue: =loop.videoName
That it is it for now any questions welcome i know the coding is not great but it works stable