Dumb down NVR

sure it does. simply add the port as you would by typing it to the browser … copied from my runtime.cfg:

org.openhab.core.ui.tiles:frontail-link-name=openHAB Log Viewer
org.openhab.core.ui.tiles:frontail-link-url=http://192.168.178.55:9001
org.openhab.core.ui.tiles:frontail-link-imageurl=data:image/png;base64,iVB[...]

It is a well supported mode:

Essentially, you set up pubkey authentication over ssh so you don’t get prompted with password, add remote host on the vscode, and it does the rest. You’ll then be opening remote paths as if you’re working locally on the remote computer.

That’s also handy to know!

Do you know how to add SVG in the same manner? Some things use svg logos (e.g. plex) I had to find a 3rd party transparent png.

You can use convert from the imagemagick package to convert between image formats.
The is also a command to set a color to transparent for formats that support transparent color.

That may be so for the most part, but it doesn’t hurt planting the seeds out there, as long as you understand that it may never be implemented. But maybe it’s a great idea that a developer would love to have too but hadn’t thought of. You never know!

Are you aware of being able to host your very own myopenhab instance somewhere.
Then you would be able to route anything without abusing something….

Of course I can use my own myopenhab and myopenhab.org for notifications. But here comes the catch google assistant integrations is lost unless I build my own app. I looked into it even did an VPs but got nowhere it was buggy maybe nodejs problem I don’t remember. I cannot give you any config or errors because I abandoned the ideea.
Also being on this subject i saw in the forums something about websokets Wich would allow to use the app using something like P2P and also have nice notifications with images and actionable buttons straight into the notification. Again just putting words together.

Deep within that is what my brain is thinking get more exposure maybe just maybe the maintainers notice and think that sounds interesting :shushing_face:

never checked but my guess:

org.openhab.core.ui.tiles:frontail-link-imageurl=data:image/svg;base64,<Base64-coded svg image>
1 Like

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.