Just a side track I found this post a guy is trying to do something similar https://community.openhab.org/t/static-folder-is-not-traversed/138103 is this allowed trough myopenhab ? I guess not but just asking.
It’s a hell of a lot more work, but it’s not lost. You just have to do all the back end work to connect your instance to Google. There are instructions in the docs for the cloud server.
Not really. Push notifications are implemented through the two phone vendor’s APIs. The mere presence of websockets doesn’t buy you anything here.
There is a way to increase the odds that someone will implement something, see here:
Introducing BountySource for funded development - Announcements - openHAB Community
You do need to make sure the bounty is realistic and achievable so best to define it on the forum first which is what your thread is doing and then see how much support there is by using a bounty.
There are a number of suggestions in this thread that could be developed further into a more concrete solution to put a bounty on.
So guys this is my work till now this is the widget:
uid: CameraHistory
tags: []
props:
parameters:
- context: item
label: Select the Camera (Equipment)
name: camera
required: true
type: TEXT
- description: "example: http://192.168.1.2:8080/ipcamera/CameraUniqueID/"
label: Base URL
name: cameraBaseURL
required: true
type: TEXT
- label: Custom Label for card
name: customLabel
required: false
type: TEXT
timestamp: Sep 5, 2023, 4:27:45 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:
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: f7-list-item
config:
accordionItem: true
title: ="Select Video - " + (vars.selectedVideoName || 'No video selected')
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
how can i make this list sorted by day hour and then minutes ?
then in order to bypass the ipcamera 50 entry limit i made a script that record automatically when something changed to open and then also delete files after a certain period this is the script
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 = "CameraRecordingRule"; // Common name for the rule and logs
let folderPathString = '/media/storage/ipcamera1'; // 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
let lastNFiles = 500; // Number of most recent files to keep
//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_ipcamera1"; // Base item name
let cameraConfigString = "ipcamera:generic:ipcamera1"; // 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();
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();
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;
}
files.push({ name: path.getFileName().toString(), time: fileTime });
}
// Sort the .mp4 files by time
files.sort((a, b) => b.time - a.time);
const lastNFilesList = files.slice(0, lastNFiles).map(file => file.name).join(", ");
items.getItem(baseItemName + '_files_list').sendCommand(lastNFilesList);
items.getItem(baseItemName + '_files_number').sendCommand(Math.min(lastNFiles, 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: ` ${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...`);
}
}
});
I welcome any comments on how to improve this or even integrate the script functionality directly into ipcamera binding.
I am also looking into how to do my own openhab hosting reverse proxy vpn etc all those options we will see. But hope its a step further for someone else to have a very basic nvr in openhab.
Ups I just noticed that the widget when you select a file opens a popup also. I will stop for now I need a break.
use webframe
component instead.
For the rest of your issues, (like sorting, entry limit) as I said, there is a working example already available.
Enough for now got it an state where i am happy. Got rid of the the popup so now it’s all one nice widget. It loads by default the slow stream mjpg then when clicked a live stream also with audio then the list with all recordings. Happy with it so far. I will try adding in the JavaScript some kind of sorting system tommorow will see.
I updated the post with the widget before.
Wait wait now I noticed your previous post about the widget you done but that just schools trough videos what I am trying to accomplish here is a small stupid NVR widget so instead of what I have now a potentially long list of videos Wich works by the way all the way to 1000 without crashing a collapsible list that first shows days then you select it gives another list with all the hour inside and then you select and you get all the ones withing that hour. I hope I made myself clear. I will see where this takes me when I have more time. But so far I am really happy have my own openhabNVR without any external bulky software.
ok small update to the widget added button for starting or stopping the stream. Still trying to wrap my head around how to have a list that parses the items and sorts them by month day and hour …
guys i am trying to use a datetime item to update the list to specific files within that time range but the i never get the button to send this is my config
- component: oh-input-card
config:
outline: true
clearButton: true
inputmode: text
footer: =items[props.item_date].state
item: =props.item
placeholder: =items[props.item_date].state
title: Set timeframe
type: datetime-local
sendButton: true
defaultValue: true
Any ideeas ?
The widget I’ve published to the Marketplace doesn’t look much different:
uid: rlk_datetime_standalone
tags:
- marketplace:127966
props:
parameters:
- description: Label for the widget
label: Label
name: label
required: false
type: TEXT
- context: item
description: An item to control
label: Item
name: item
required: false
type: TEXT
parameterGroups: []
timestamp: Jul 27, 2021, 10:06:53 AM
component: oh-input-card
config:
clearButton: false
footer: =items[props.item].state
inputmode: text
item: =props.item
outline: true
placeholder: =items[props.item].state
sendButton: true
title: =props.label
type: datetime-local
The only difference is the clearButton
. Try it in different browsers. It’s actually the browser that renders this UI widget and each browser is slightly different. See the discussion on the marketplace entry for more details.
got it working it was my fault. Thank you @rlkoshak @matt1 @Oliver2 @JimT for all the help pushing me to find a solution to my problem. I posted my solution by editing the original post.
Nice coding. I still do not understand where the advantage is (no offense - it is me who doesn‘t get it).
If you execute this command in a shell script it deletes files in a folder except the 20 newest ones:
ls -tp | tail -n +21 | xargs -I {} rm -- {}
If you execute this command, it returns the remaining 20 files sorted by time as an array which you can directly process in your rule:
ls $folder -1t
If you move your file operations to the level dealing with files (i.e. your OS) to my understanding you get a far more robust solution.
I tried to do the same because I wanted to have all my code in JS, but I stepped back from this approach as my code grew like yours😀
I you follow the tread the point of this is the wife everything you see is based on her feedback . Not having to deal with another app and not having something complicated as a full fledged NVR.
Simple widget with simple navigation easy to explain. I will probably add options straight into the widget to adjust recording time deletion period etc so stay tuned.
Yes yes I first played with the exec binding before starting the route of importing nio.
Why only JavaScript well before this I did all my automations in dsl and then all of a sudden our alarm craped out and I have another one for testing from risco but needed a alarm so here started learning JavaScript. Now I am no expert in coding Linux etc my field has nothing to do with all this but I am trying and since I discovered openhab I liked it because it follows has alot of similarity with knx. Thinking about now I think Kai started this to have a visu and a way to automate stuff on his installation
Please have a look at Frigate. It is simple to use, has a nice interface for looking up events and past history, and it deals with storage, cycling old snapshots, clips etc.
I started with blueiris and was resistant to change but so glad I made the change.
Frigate also offers nice Mqtt messages which make things easy to automate in openhab.
It can even send snapshots of events which you can save in an image item and display that on your sitemap.
Or you could perhaps just browse/expose its snapshot or clips folder?
The time I spent doing what I did now probably was just 5 minutes of spinning up a docker image and configuring an yaml file Wich I agree I get also object detection with Google coral etc.
But what about learning and also making other people happy ? Like for example trying to see what is the best way to remotely access openhab came as a necessity out of doing this myself and now I am looking into load balancer stuff so a fun little next project.
I only have 4 cameras and use NVIDIA GPU. Haven’t been able to buy a USB coral and the GPU seems fine.
By the way I’m curious to see your solution. Would you mind posting a screenshot?
Well in my case motion detectors are placed next to camera with a light so it’s very convenient to start recording based on that why process stream to see if I can find something.
Sure
That looks pretty good!