Dumb down NVR

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 :slight_smile: . 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 :slight_smile:

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?

2 Likes

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


1 Like

That looks pretty good!