Dumb down NVR

Can that be done without editing the runtime.cfg file? I.E can it be done via the mainUI as I have changed everything over to that and have no files, all done via mainUI.

Thanks! Where is this /static in the file system?

It is here:
/etc/openhab/html

To test go to a file you have in the directory: I have graphviz.png
http://192.168.1.164:8080/static/graphviz.png

1 Like

I don’t think so. This is still a remaining text config hold out.

Anything placed in the conf/html folder is available internally to OH via the /static address. But you don’t need to use that if you’ve got some other accessible uri, the imageurl config should take anything that can be accessed by OH.

1 Like

not to forget,

org.openhab.core.ui.tiles:frontail-link-imageurl=

can be set with “direct” data by using data:image/png;base64 (the basicUI link is set in this way) the only thing you have to do is to encode the picture in base64 :slight_smile:

1 Like

Hmm but that doesn’t forward the port and all the calls made to frontail. So basically what we would be needed is some container environment that you can install apps ala homeasssitant and a way to publish premade containers configured to route their respective ports to openhab port so that when using myopenhab everything is routed nicely. But again the problem is here we are getting in the trap of abusing the free service to do routing.
This solution works just fine locally it opens a new tab with your web app

In the end I believe I just want homeasssitant feature in openhab. So either I implement them myself or I shut up.
As long as I don’t have the skills to implement myself anything there is no point in this conversation just trowing out words and asking other to do it for me will lead nowhere no one is obliged to work on something they don’t have any interest in doing so. So again I apologize to everybody the more I reply the more I realize that if you want something do it yourself then ask if you get stuck along the way.

By the way @JustinG I use that widget that you made to confirm action in a lot of places especially turning all light off and the wife also appreciated that so thank you for that.

Do you have an example of how did you setup that ? Sound interesting!

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.