Dumb down NVR

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

OH is neither designed to be nor suitable to be a full NVR with all the bells and whistles one would expect. It does just enough to integrate with the home automation OH provides but only provides very basic NVR functionality outside that.

You are better off looking into one of the many alternatives that are specially designed for this. Last time I did anything like this ZoneMinder, Shinobi, and Blue Iris were the ones I looked at. I’m sure there are more options these days.

Well why not all the other projects are just some fancy wrappers around ffmpeg. Let me explain why so far ipcamera binding is triggering a record command to ffmpeg with some parameters and a file location for a certain amount of time that can be started using a rule triggered by anything you like. And just by that you have a simple recorder. One can argue you need million of features like video processing but in a simple home automation you just want to record when certain sensors detect something and also some cameras have build in image processing that can be pulled into openhab and compared with other sensors for false alarm. Now that we have that out of the way is there anything that can be done to have an file explorer feature build into openhab that also will work using myopenhab?

If that’s all you think they are and all they do I welcome you to contribute something to OH to make this happen. I know better but sometimes people need to experience things themselves.

Nothing I know of. There is nothing built into core that would support this and nothing in any of the UIs that would support this. You can host webpages with JavaScript in $OH_CONF/html which gets served up by OH’s webserver to http://<host or IP>:8080/static. Maybe you can create a web page that does this. But if you want stuff like “video support playback text file viewer and maybe why not editor” you’re not going to be able to do that with strictly a static web page and some JavaScript. You’re going to have to build in some server side stuff as well and we are right back to reimplementing something others have already implemented better than we ever will.

2 Likes

That is because of license issues. To process h265 and other formats in some cases you need a license and you are restricted in what you can do without paying for licenses. If a user installs ffmpeg and that handles the formats then… It is hard in an opensource project where you can not charge $ to cover license fees, nor can you get a lawyer to advise you on the topic.

Why don’t you look at Frigate? It is an example of something better then a basic file explorer and is created by people that share a wish for what you want. You can create a shortcut link that opens this UI from openHAB.

Frigate NVR

The 50 entry limit is easy to increase and is set at this as I had no idea just how large a STRING length the channel supports, so I randomly chose this length without doing any research. I did not like the idea of having a potentially memory hungry channel that keeps growing in size. If you want a larger size just create a github issue. You’re the first person to question or mention it.

Please do, would love to hear how easy is was and what you have working and how you use it.
I personally tend to not want to sit down and review footage taking me away from the more important things in my life. I use Telegram to send the video files to an external server that keeps an offsite copy
and also allows me to browse the files in realtime as the events occur. This way I dont waste time at home.

2 Likes

Assuming it’s the same as the maximum size a String can be, it’s probably around 2 GB.

Well that should not be an issue because I can make my own string and parse the folder to update it I am working on that right now in javascript

Hmm that is interesting :thinking: .

I also don’t like spending time on this and I also like the ideea of the right tool for the job. But see here the problem is million apps or web apps I hate that.

And let’s be honest all the other projects out there have basic file explorer and editor build in the frontend but my skill set is not there to implement it myself.

I would really love to see the community come together to vote for such a useful feature. What do you guys think should I open an request on GitHub maybe @ysc can chime in to squish my dreams :slight_smile:

That’s not how it works. Only two people’s votes matter: the person who volunteers to do the work and creates the PR, and at least one of the maintainers of the repo. Sometimes it’s the same person. No one else’s vote matters. We can discuss, beg, and argue until we are blue in the face. But only those two people have anything to do with anything getting actually implemented and added to the baseline.

It’s always worth opening issues for feature requests like this, but nothing comes of it until you get those two people on board.

From a security perspective, I’ve already got concerns in some of the ways that people expose their OH instance to the internet. A file browser capability would only serve to make my concerns even worse. But that’s just my opinion. Like I said, my vote doesn’t really count.

3 Likes

Well let’s pray :pray: and see

Well I won’t then maybe the maintainers are looking in the forums :slight_smile:

Openhab is not secure by default. There was a guy from Belgium who did a study about adding rbac and securing openhab for his school project but lead nowhere. So if people expose openhab to the world this would make it more user friendly for people to browse their private video photos files that openhab has acces to. But hey if we go about that mentality they also open port 22 so they can ssh Into their raspberry pi what then ?
I am just trying to be optimistic maybe just maybe …

It lead nowhere cause we asked him to provide a pull request so it could be reviewed and merged, but he never did☹️

Agreed.

Twice actually (another team of researches wrote a paper on this too). And in both cases they published a paper and dropped it without contributing back to OH. There is a PR open as well from some non-academic contributors but that one is stalled I think. It’s a hard problem really and when done right, it will touch almost everything.

But you’ve asked for more than that. You want more than just videos and even some edit ability which means ability to write as well. And when people do stupid things, OH still gets blamed.

Awhile back some researchers found thousands of MQTT brokers exposed to the internet with no security enabled. Was the headline “thousands of MQTT admins don’t know what they are doing”? No it was “MQTT is not secure!”.

ssh is reasonably secure by design and by default it can be configured in such a way that it’s reasonably safe to expose to the internet. I still wouldn’t recommend it. It’s not a comparison.

C’mon I am not getting a break why everything has to be so complicated. I just want to see my videos in my folder as nice list then click on it bum videoplayer and all with my openhab app with myopenhab.org :frowning: and maybe just maybe edit some scripts items etc on the fly. And still being on the topic the file explorer could also display logs live and browse or search all in the comfort of webui using also myopenhab (hey one can dream). If you really think about it’s a useful tool.
Anyways I stop for now.
I am in the process of modifying the original browsing widget for ipcamera and writing a JavaScript to record parse files etc a simple NVR like j said.

1 Like

You can, but I disagree that people should do this when the binding has the ability already to handle it for you. The reason it has a limit, is it can be the cause of a memory leak if no one clears the string out and it constantly grows over time to hold 2gb of data which is a problem for a raspb. pi with limited ram in the first place. I felt that 50 visitors to my front door was a good place to put the limit, but it would be very easy to raise this to 200 or another value that users feel is a good compromise. The issue is that people tend to blindly add all channels and create items even if they do not use them, this opens up the chewing of memory that they do not understand, nor should they need to know that level of detail IMHO as it should just work. Since I am the maintainer, your speaking directly to someone who is happy to raise the value if you give some feedback on what a good compromise is as a limit?
EDIT: A better way would be to make this a config that a user can change to any value and leave it at a 50 default value. I would need to look but this should not be hard to change to doing this.

Please don’t feel dis-heartened. It is complicated as I already explained you have to consider licensing fees for video formats, @rlkoshak has raised security concerns about file browser features and there are no doubt many more angles. It’s a matter of finding a feature that would help move the project forward towards your end goal.

I would like to propose that instead of using the myopenhab cloud and app, that you look at using wiregaurd to connect to your server directly with and then you have full access like your actually still at home, even when your away. You can add a shortcut link to any file browser or NVR app you like to the launch section of openHAB just like the way frontail is done with an openhabian install.

Great, look forward to seeing you share what you make back as a tutorial here in the forum. I think a discussion on what your wanting to achieve would be a good idea with some screen shots of what you’re currently got working to excite and encourage others to do the same.

1 Like

Why don’t you just install Samba and VPN?

@matt1 @JimT the problem for me it’s the other members of the family Wich are not used to it I already have wireguard setup with DNS resolution but that drop from time to time also it requires extra steps related to operate the house and also establish the connection again. I know I sound like a broken record but if you look at homeassitant homebridge etc I sometime question myself. I don’t want to to switch I love openhab and I want to see it work for me me no matter if I get flagged and people say but it’s not possible you have to do it this way.
Let’s leave it like it is until I get a working setup that works for me to publish my finding then we discuss

Why do you want to use myopenhab to watch your videos ?
Remote ? You don‘t really want to route your video streams through our for free service ?

Every now and again I wonder what people want to use openHAB for. I really often ask myself what this usecase has in common with home automation……

2 Likes

I just need to be able to go back in openhab see what happened all within the comfort of myopenhab.

I don’t have a need to stream high definition or anything all streams are set at 640x480 to record.
Also it’s not my intention to abuse the free service.

It’s in the name open home automation bus. I do understand the concept I work with knx and openhab concept is based on the same mentality I believe @Kai started this because he had knx in his house and wanted visualisation and some way to automate stuff in his house but then think about it why do we have an interface in the first place ? If the purpose of the project it’s to get all data in a common bus with predefined data points to be able to automate with them why the webui is used for visualization not just for administration and only admin work ?

I believe if that is the case then openhab should resemble something like nodered just an automation platform and not publish itself as a one stop shop for home related stuff.

Maybe I am tripping here and rambling stupid stuff I don’t understand. Then I apologize to the community and I will shut up. So if @hmerk you think this post has no valid points and it’s just a wish list please close the topic or rename etc.

I only posted this because lately I wanted more security related automation in the house so that is why I started this journey to see how far openhab would go related to security of the house see also my other post of an burglar alarm https://community.openhab.org/t/openhab-burglar-alarm-diy-not-a-real-one/148385

If that’s what you want, that’s fine, but I don’t understand why you then „ask“ for a file explorer?

If that is your core requirement, then simply go ahead and build it yourself (as you mentioned in your first post). Shouldn‘t be too complicated.
As a starter have a look here which already does the same thing based on camera images. With some changes it will be possible with video, too.

I wouldn‘t pursue this approach. Add a VPN-profile to your mobile phone so that everytime your app accesses an internal IP-address, it opens automatically a VPN connection first. This works at least for iPhones as I already have it implemented this for myself.