Startseite » Kamera-Popup-Widget für OpenHAB

Kamera-Popup-Widget für OpenHAB

Dieses Kamera-Widget ist Teil meines Pokemon-Floorplans und besteht aus mehreren Bausteinen und ermöglicht es, Live-Bilder und Streams aus verschiedenen Kameras direkt in der OpenHAB-Oberfläche darzustellen.
Zusätzlich können – je nach Kamera – Steuerungsfunktionen wie PTZ (Pan, Tilt, Zoom), Presets, oder Alarmsignale (Bewegung/Audio) eingebunden werden.

Als Eingaben können KI-gestützte RaspberryPi-Kameras, ESP32Cam oder andere IP-Kameras dienen.

Die Umsetzung besteht aus vier Teilen:

  1. Symbol-Widget zum Öffnen des Kamera-Popups
  2. Darstellungs-Widget mit Steuerung und Stream-Ausgabe
  3. HLS-Player (für Kameras mit HLS-Stream)
  4. MJPEG-Player (für einfache MJPEG-Streams)

Symbol-Widget (Popup-Trigger)

Dieses kleine Widget ist nur ein Button mit Kamerasymbol, das ein Popup öffnet.
Im Code wird definiert:

  • welche URLs für den Stream (MJPEG oder JPEG-Vorschau) genutzt werden,
  • welche Items für Steuerung oder Alarme angebunden sind.

Für Laien erklärt:
Das Symbol ist der „Eingang“ zum Kamera-Fenster. Klickt man darauf, wird das größere Darstellungs-Widget geöffnet.

Widget-Code
uid: ha_cam_popup
tags: []
props:
  parameters:
    - label: MJPEG URL
      name: camera
      required: true
      type: TEXT
    - label: JPEG URL
      name: preview
      required: true
      type: TEXT
    - context: item
      label: PTZ-Item
      name: ptz
      required: false
      type: TEXT
    - context: item
      label: Pan-Item
      name: pan
      required: false
      type: TEXT
    - context: item
      label: GoToPreset-Item
      name: gotopreset
      required: false
      type: TEXT
    - context: item
      label: Motion-Alarm-Item
      name: motionalarm
      required: false
      type: TEXT
    - context: item
      label: Audio-Alarm-Item
      name: audioalarm
      required: false
      type: TEXT
timestamp: Jun 17, 2025, 1:33:33 AM
component: oh-button
config:
  action: popup
  actionModal: widget:ha_cam
  actionModalConfig:
    audioalarm: =props.audioalarm
    camera: =props.camera
    gotopreset: =props.gotopreset
    motionalarm: =props.motionalarm
    pan: =props.pan
    preview: =props.preview
    ptz: =props.ptz
  iconF7: videocam_circle
  iconSize: 30
slots: {}

Darstellungs-Widget (Kamera-Ansicht)

Hier passiert die eigentliche Magie:

  • Der Stream (HLS oder MJPEG) wird im OpenHAB-Frontend angezeigt.
  • Steuerungselemente (Pfeile, Stop-Symbol, Presets) erscheinen nur dann, wenn die entsprechenden Items gesetzt sind.
  • Bewegungs- oder Audio-Alarm werden als kleine Symbole eingeblendet.

Für Laien erklärt:
Das ist das große Fenster, in dem man das Kamerabild sieht. Links/rechts/hoch/runter-Pfeile steuern die Kamera, falls diese PTZ unterstützt. Zusätzlich gibt es Symbole für Bewegung und Ton, die bei einem Alarm sichtbar werden.

Widget-Code
uid: ha_cam
tags: []
props:
  parameters:
    - label: MJPEG URL
      name: camera
      required: true
      type: TEXT
    - label: JPEG URL
      name: preview
      required: true
      type: TEXT
    - context: item
      label: PTZ-Item
      name: ptz
      required: false
      type: TEXT
    - context: item
      label: Pan-Item
      name: pan
      required: false
      type: TEXT
    - context: item
      label: GoToPreset-Item
      name: gotopreset
      required: false
      type: TEXT
    - context: item
      label: Motion-Alarm-Item
      name: motionalarm
      required: false
      type: TEXT
    - context: item
      label: Audio-Alarm-Item
      name: audioalarm
      required: false
      type: TEXT
timestamp: Jul 5, 2025, 12:17:58 AM
component: f7-card
config:
  style:
    --f7-card-margin-horizontal: 0px
    height: calc(100% - 5px)
    margin: 0
    width: 100%
slots:
  default:
    - component: oh-link
      config:
        action: command
        actionCommand: left
        actionItem: =props.ptz
        class: card-prevent-open
        iconF7: arrow_left
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.ptz) ? 'true' : 'false'"
    - component: oh-link
      config:
        action: command
        actionCommand: up
        actionItem: =props.ptz
        class:
          - card-prevent-open
        iconF7: arrow_up
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.ptz) ? 'true' : 'false'"
    - component: oh-link
      config:
        action: command
        actionCommand: false
        actionItem: =props.pan
        class:
          - card-prevent-open
        iconF7: stop_circle
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.pan) ? 'true' : 'false'"
    - component: oh-link
      config:
        action: command
        actionCommand: down
        actionItem: =props.ptz
        class: card-prevent-open
        iconF7: arrow_down
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.ptz) ? 'true' : 'false'"
    - component: oh-link
      config:
        action: command
        actionCommand: right
        actionItem: =props.ptz
        class: card-prevent-open
        iconF7: arrow_right
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.ptz) ? 'true' : 'false'"
    - component: oh-link
      config:
        action: options
        actionItem: =props.gotopreset
        class: card-prevent-open
        iconF7: list_number
        iconSize: 40
        style:
          color: var(--f7-card-header-text-color)
          margin: 0 0.4em
          opacity: 0.5
        visible: "=(props.gotopreset) ? 'true' : 'false'"
    - component: oh-link
      config:
        iconF7: eye
        iconSize: 24
        style:
          color: var(--f7-card-header-text-color)
          opacity: "=(@@props.motionalarm === 'ON') ? '0.5' : '0'"
          position: absolute
          right: 0.2rem
          top: 7.8rem
        visible: "=(props.motionalarm) ? 'true' : 'false'"
    - component: oh-link
      config:
        iconF7: ear
        iconSize: 24
        style:
          color: var(--f7-card-header-text-color)
          left: 0rem
          opacity: "=(@@props.audioalarm === 'ON') ? '0.5' : '0'"
          position: absolute
          top: 7.7rem
        visible: "=(props.audioalarm) ? 'true' : 'false'"
    - component: oh-webframe
      config:
        height: 100%
        lazy: true
        src: |
          =props.camera.endsWith('.m3u8') ?
            "/static/hls_player.html?src=" + props.camera
            : "/static/mjpeg_player.html?src=" + props.camera
        style:
          margin: 0px

HLS-Player (HTML-Seite)

Dieser Player ist für moderne Kameras, die einen HLS-Stream ausgeben.

HLS ist sehr Bandbreitenschonen, hat aber durch seine Technik eine leichte Verzögerung.

Für Laien erklärt:
HLS ist eine Streaming-Technik, wie sie auch Netflix oder YouTube verwenden. Dadurch wird das Video gestückelt übertragen und läuft stabiler über das Internet.

Das Darstellungs-Widget benötigt es und versucht es unter /static/hls_player.html zu erreichen. In vielen Fällen entspricht das /etc/openhab/html/hls_player.html auf einem Linux-Filesystem deines OpenHAB-Servers.

Widget-Code
<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>HLS Video</title>
  <script src="hls.js"></script>
</head>
<body style='background: black;'>

<video id="video" controls width="100%"></video>

<script>
  // URL-Parameter lesen
  // https://deine-seite.de/video.html?src=https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8
  const params = new URLSearchParams(window.location.search);
  const videoSrc = params.get('src');

  if (videoSrc) {
    const video = document.getElementById('video');

    if (Hls.isSupported()) {
      const hls = new Hls({
		liveSyncDurationCount: 2
	  });
      hls.loadSource(videoSrc);
      hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = videoSrc;
    }
  } else {
    document.body.insertAdjacentHTML('beforeend', '<p>Kein Video-Link angegeben.</p>');
  }
</script>

</body>
</html>

MJPEG-Player (HTML-Seite)

Dieser Player ist für einfache Kameras, die nur MJPEG ausgeben können.
Das Bild wird dabei als Endlosschleife einzelner JPEG-Bilder übertragen – eine ältere, aber sehr kompatible Technik.
MJPEG verbraucht normalerweise einen großen Teil an Bandbreite.

Für Laien erklärt:
Man sieht quasi eine schnelle Bildergalerie, die wie ein Video wirkt. Es gibt zusätzlich einen Vollbild-Knopf, um das Kamerabild zu maximieren.

Das Darstellungs-Widget benötigt es und versucht es unter /static/mjpeg_player.html zu erreichen. In vielen Fällen entspricht das /etc/openhab/html/mjpeg_player.html auf einem Linux-Filesystem deines OpenHAB-Servers.

Widget-Code
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Einfacher MJPEG-Player mit URL-Parameter</title>
<style>
  body {
    margin: 0;
    font-family: Arial, sans-serif;
	background-color: black;
  }
  #player-container {
    position: relative;
    max-width: 640px;
    margin: 0;
    border: 0;
    border-radius: 6px;
    overflow: hidden;
    background: #000;
  }
  #mjpeg-frame {
    width: 100%;
    border: none;
    display: block;
  }
  #controls {
    text-align: center;
    margin: 10px auto;
  }
  button {
    padding: 8px 16px;
    font-size: 1rem;
    cursor: pointer;
    border-radius: 4px;
    border: none;
    background-color: #007bff;
    color: white;
  }
  button:hover {
    background-color: #0056b3;
  }
  #player-container.fullscreen {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw !important;
    height: 100vh !important;
    max-width: none !important;
    aspect-ratio: auto !important;
    z-index: 9999;
    border-radius: 0;
  }
</style>
</head>
<body>

<div id="player-container">
  <img
    id="mjpeg-frame"
    src=""
	controls
  />
</div>

<div id="controls">
  <button id="toggle-fullscreen">Maximieren</button>
</div>

<script>
  // Funktion um URL-Parameter auszulesen
  function getUrlParameter(name) {
    name = name.replace(/[\[\]]/g, '\\$&');
    const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
    const results = regex.exec(window.location.href);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, ' '));
  }

  const player = document.getElementById('player-container');
  const iframe = document.getElementById('mjpeg-frame');
  const btn = document.getElementById('toggle-fullscreen');

  // MJPEG URL aus Parameter lesen
  const mjpegUrl = getUrlParameter('src') || 'http://dein-mjpeg-stream-url/mjpeg';
  iframe.src = mjpegUrl;

  btn.addEventListener('click', () => {
    if (!document.fullscreenElement) {
      if (player.requestFullscreen) {
        player.requestFullscreen();
      } else if (player.webkitRequestFullscreen) {
        player.webkitRequestFullscreen();
      } else if (player.msRequestFullscreen) {
        player.msRequestFullscreen();
      }
    } else {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      }
    }
  });

  document.addEventListener('fullscreenchange', () => {
    if (document.fullscreenElement) {
      player.classList.add('fullscreen');
    } else {
      player.classList.remove('fullscreen');
    }
  });
</script>

</body>
</html>

1 Kommentar zu „Kamera-Popup-Widget für OpenHAB“

  1. Pingback: Interaktiver Floorplan in OpenHAB mit Effekten

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.

Translate »