Does anyone have an example for „in case of alarm, send a picture of the incident along with the notification to the app“?
@Cplant you can make this very simple or very complicated, and like I said previously you can build this with the basic OpenHAB rule engine:
When alarm item changes from off to on;
Broadcast notification with the title, text and media url carrying either the item:nameOfItemWithCameraSnapshop or the local url to the snapshot. Only snag is that this isn’t working right now so maybe use the telegram binding for now?
With a TTS service (I use piper) you can even tell Google to warn you. Pretty cool.
Quick question: I got the binding working this morning, but now (some hours later) I can’t get the Server-Thing online:

This is what the camera-Thing says:

The logs appear as if the connection can’t be established (“refused”?)
2025-09-28 21:33:41.705 [INFO ] [nal.handlers.frigateSVRServerHandler] - - Frigate server is offline
2025-09-28 21:33:41.706 [ERROR] [nternal.helpers.frigateSVRHTTPHelper] - ExecutionException: java.net.ConnectException: Connection refused
2025-09-28 21:33:41.706 [WARN ] [nal.handlers.frigateSVRServerHandler] - unable to get version string
Does anyone have an idea? The MQTT-broker is online (I’m using it for other purposes as well). The only thing I really changed was opening port 1984 in the frigate yaml-docker configuration, and playing around with the detector model a bit, but that can’t be it?
Update: I found the root cause. When restarting the frigate container, its IP address changes (used to be 172.18.0.2 before the container reboot, now its 172.18.0.6).
![]()
Does anyone know how to a) fix the IP address or b) define an “alias” of some sort that cann be defined in openHAB that does not change?
In the docker compose, specify a static ip…? That’s a docker question though
Or just point at your host ip:5000 and it should work if it’s in host network.
Regarding showing the feed in OpenHAB, this is something to try:
Better Late Than Never ![]()
Well, I finally got around to implementing this… 3 months later. Why the delay? I was waiting for my Raspberry Pi 5 with Hailo-8 AI accelerator to arrive and get properly set up!
But now it’s running beautifully, and I wanted to share my complete implementation.
My Setup
Hardware:
- Raspberry Pi 5 (16GB) with Hailo-8 AI accelerator (running Frigate NVR)
- 8x IP Cameras (Hikvision, H.265 streams)
- QNAP NAS for snapshot storage (rolling 50 snapshots per camera)
- Raspberry Pi 5 (8GB) running openHAB 5.0.3
Software Stack:
- Frigate NVR (running on the Pi 5 with Hailo-8)
- MQTT (Mosquitto) for Frigate ↔ openHAB communication
- openHAB 5.0.3 (on separate Pi 5 8GB)
- Frigate Binding for openHAB (provides event JSON, clip URLs, etc.)
- Telegram for direct photo/video delivery
- openHAB Cloud for push notifications with images
The Example: Front Door Camera Rule
This is my most complex camera rule because it handles three different scenarios:
- Motion Detection → Person/car detected → Snapshot with bounding box
- Cat Doorbell → Cat enters specific zone → “Let me in!” notification with snapshot
- Physical Doorbell → Button pressed → Latest frame (even if no motion event active)
I’ll share the complete rule file because it demonstrates:
- Event-based triggering (JSON from Frigate Binding)
- Snapshot download with retry logic + thumbnail fallback
- Latest frame download (for doorbell without motion)
- NAS storage + Image Item buffer (works with openHAB Cloud)
- Direct openHAB Cloud notifications with media attachments
- Telegram photo delivery
- Zone-based detection (cat doorbell)
- Error isolation and timeout protection
Key Concepts
1. Why Hybrid Storage (NAS + Image Items)?
The Problem:
- Direct URLs to Frigate snapshots don’t work with openHAB Cloud
- Storing Base64 in Items works but is memory-intensive
The Solution:
- Download snapshot from Frigate
- Save to NAS (permanent storage, last 50 per camera)
- ALSO save to Image Item (5-buffer round-robin for notifications)
- Image Item works with openHAB Cloud (Base64 sync)
2. Round-Robin Buffer (5 Image Items)
Instead of one Image Item per camera, I use 5 per camera:
Frigate_Snapshot_FrontDoor_1
Frigate_Snapshot_FrontDoor_2
Frigate_Snapshot_FrontDoor_3
Frigate_Snapshot_FrontDoor_4
Frigate_Snapshot_FrontDoor_5
Why?
- Multiple events within seconds don’t overwrite each other
- Each notification gets its own image (no race conditions)
- Buffer rotates: 1 → 2 → 3 → 4 → 5 → 1
3. Latest Frame vs Event Snapshot
Event Snapshot:
- URL:
http://frigate:5000/api/events/{eventId}/snapshot.jpg - Pros: Has bounding box, tied to detection
- Cons: May not exist immediately, disappears after event ends
Latest Frame:
- URL:
http://frigate:5000/api/{camera}/latest.jpg - Pros: ALWAYS available, current frame
- Cons: No bounding box, no object detection
When to use what:
- Motion detection → Event Snapshot (with retry + thumbnail fallback)
- Doorbell button → Latest Frame first (more current), Event Snapshot as fallback
4. Telegram Photos vs Telegram Videos
My setup sends:
- Photos for snapshots (via
sendTelegramPhoto()using Base64 from Image Item) - Videos for clips (via
sendTelegramVideo()using Frigate clip URL)
This gives immediate visual feedback in Telegram alongside the openHAB Cloud push notification.
The Complete Rule File
File: automation/js/frigate_frontdoor.js
Items Required
// Frigate Binding Items (provided by binding)
String frigate_cam_frontdoor_Event_JSON "Front Door Event JSON" { channel="mqtt:frigateCamera:..." }
String frigate_cam_frontdoor_Current_Event_ID "Front Door Event ID" { channel="mqtt:frigateCamera:..." }
DateTime frigate_cam_frontdoor_Current_Start_time "Front Door Start Time" { channel="mqtt:frigateCamera:..." }
String frigate_cam_frontdoor_URL_for_current_event_clip "Front Door Clip URL" { channel="mqtt:frigateCamera:..." }
// Image Item Buffer (5 items for round-robin)
String Frigate_Snapshot_FrontDoor_1 "Front Door Snapshot 1"
String Frigate_Snapshot_FrontDoor_2 "Front Door Snapshot 2"
String Frigate_Snapshot_FrontDoor_3 "Front Door Snapshot 3"
String Frigate_Snapshot_FrontDoor_4 "Front Door Snapshot 4"
String Frigate_Snapshot_FrontDoor_5 "Front Door Snapshot 5"
// Control Items
Switch Advanced_Logging "Advanced Logging"
Switch Camera_FrontDoor_AutoRecord "Front Door Auto Record"
Switch Doorbell_Virtual "Virtual Doorbell Button" { expire="2s,command=OFF" }
Switch CatDoorbell_FrontDoor_Enabled "Cat Doorbell Enabled"
Contact DoorContact_FrontDoor_State "Front Door Contact"
JavaScript Rule
const { rules, items, triggers, actions } = require('openhab');
// ============================================================
// TRACKING VARIABLES
// ============================================================
let entrance_door_timer = null;
let processed_snapshots = new Set();
let last_snapshot_timestamp = 0;
let last_catbell_time = 0;
let cleanup_timer_snapshots = null;
let current_buffer = 1; // Round-robin: 1 → 2 → 3 → 4 → 5
const SNAPSHOT_COOLDOWN_MS = 5000; // 5 seconds between snapshots
const SNAPSHOT_CACHE_CLEANUP_MS = 120000; // 2 minutes
const CATBELL_COOLDOWN_MS = 60000; // 60 seconds between cat doorbell notifications
const MAX_CACHE_SIZE = 50;
const RECIPIENT_TIMEOUT_MS = 15000; // 15 seconds per Telegram recipient
// ============================================================
// HELPER FUNCTIONS
// ============================================================
// Logger with Advanced Logging support
const log = (message, level = 'info') => {
try {
const advancedLogging = items.getItem('Advanced_Logging')?.state === 'ON';
if (advancedLogging || level === 'error' || level === 'warn') {
const prefix = 'FRIGATE_FRONTDOOR';
switch(level) {
case 'debug':
if (advancedLogging) console.debug(`[${prefix}] ${message}`);
break;
case 'warn':
console.warn(`[${prefix}] ${message}`);
break;
case 'error':
console.error(`[${prefix}] ${message}`);
break;
default:
if (advancedLogging) console.info(`[${prefix}] ${message}`);
}
}
} catch (e) {
console.error(`[FRIGATE_FRONTDOOR] Logger error: ${e}`);
}
};
// Send notification via openHAB Cloud
// IMPORTANT: Replace 'user1@example.com', 'user2@example.com' with your actual email addresses
// registered with openHAB Cloud (myopenhab.org)
const sendNotification = (message, options = {}) => {
try {
const {
title = "Home Security",
tag = "Camera Motion",
icon = null,
mediaUrl = null,
id = `camera-${Date.now()}`,
recipients = [] // Array of email addresses
} = options;
if (recipients.length === 0) {
log(`No recipients specified for notification`, 'warn');
return;
}
// Handle media attachments (send individually per recipient)
if (mediaUrl) {
recipients.forEach((email, index) => {
try {
const uniqueId = `${id}-${index}`;
const builder = actions.notificationBuilder(message || " ")
.withTitle(title)
.withTag(tag)
.withReferenceId(uniqueId)
.withMediaAttachmentUrl(mediaUrl)
.addUserId(email);
if (icon) {
builder.withIcon(icon);
}
builder.send();
log(`📧 Notification with media sent to: ${email}`, 'debug');
} catch (error) {
log(`Error sending notification to ${email}: ${error}`, 'error');
}
});
} else {
// Standard notification (batch send)
const builder = actions.notificationBuilder(message)
.withTitle(title)
.withTag(tag)
.withReferenceId(id);
if (icon) {
builder.withIcon(icon);
}
recipients.forEach(email => builder.addUserId(email));
builder.send();
log(`📧 Notification sent to ${recipients.length} recipients`, 'debug');
}
} catch (error) {
log(`Error sending notification: ${error}`, 'error');
}
};
// Format timestamp for notifications
const formatTimestamp = (itemOrString) => {
try {
if (!itemOrString || itemOrString === 'NULL' || itemOrString === 'UNDEF') {
return null;
}
let timestampString;
if (typeof itemOrString === 'object' && itemOrString.toString) {
timestampString = itemOrString.toString();
} else {
timestampString = itemOrString;
}
if (!timestampString || timestampString === 'NULL' || timestampString === 'UNDEF') {
return null;
}
let cleanString = timestampString.replace(/\+.*$/, '').replace('T', ' ');
const parts = cleanString.split(' ');
if (parts.length !== 2) return null;
const dateParts = parts[0].split('-');
const timeParts = parts[1].split(':');
if (dateParts.length !== 3 || timeParts.length < 2) return null;
const year = parseInt(dateParts[0]);
const month = parseInt(dateParts[1]) - 1;
const day = parseInt(dateParts[2]);
const hour = timeParts[0];
const minute = timeParts[1];
const date = new Date(year, month, day);
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const weekday = weekdays[date.getDay()];
return `${weekday} ${day.toString().padStart(2, '0')}.${(month + 1).toString().padStart(2, '0')}.${year} at ${hour}:${minute}`;
} catch (e) {
log(`Error formatting timestamp: ${e}`, 'error');
return null;
}
};
// ============================================================
// FEATURE 1: Event Snapshot Download → NAS + Image Item
// ============================================================
const downloadSnapshotToBuffer = async (eventId) => {
const snapshotUrl = `http://192.168.1.100:5000/api/events/${eventId}/snapshot.jpg?bbox=1`;
const thumbnailUrl = `http://192.168.1.100:5000/api/events/${eventId}/thumbnail.jpg`;
const nasPath = '/mnt/nas/snapshots/frontdoor';
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
let finalUrl = snapshotUrl;
let usedThumbnail = false;
// ============================================
// HTTP CHECK: Is snapshot ready?
// ============================================
try {
const http = Java.type('java.net.http.HttpClient');
const request = Java.type('java.net.http.HttpRequest');
const uri = Java.type('java.net.URI');
const client = http.newHttpClient();
const testReq = request.newBuilder()
.uri(uri.create(snapshotUrl))
.timeout(Java.type('java.time.Duration').ofSeconds(5))
.build();
const testResponse = client.send(testReq, Java.type('java.net.http.HttpResponse$BodyHandlers').ofString());
const body = testResponse.body();
if (body.includes('"success":false') || body.includes('Live frame not available')) {
log(`⚠️ Snapshot attempt ${attempt}/${MAX_RETRIES} failed - not ready yet`, 'debug');
if (attempt < MAX_RETRIES) {
log(`⏳ Waiting ${RETRY_DELAY_MS / 1000}s before retry...`, 'debug');
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
continue;
} else {
log(`⚠️ Snapshot not available after ${MAX_RETRIES} attempts - using thumbnail`, 'warn');
finalUrl = thumbnailUrl;
usedThumbnail = true;
}
}
} catch (e) {
log(`⚠️ Snapshot check failed (attempt ${attempt}/${MAX_RETRIES}): ${e}`, 'warn');
if (attempt < MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
continue;
} else {
log(`⚠️ Using thumbnail after ${MAX_RETRIES} attempts`, 'warn');
finalUrl = thumbnailUrl;
usedThumbnail = true;
}
}
// ============================================
// DOWNLOAD: Get image from Frigate
// ============================================
const http = Java.type('java.net.http.HttpClient');
const request = Java.type('java.net.http.HttpRequest');
const uri = Java.type('java.net.URI');
const bodyHandler = Java.type('java.net.http.HttpResponse$BodyHandlers');
const client = http.newHttpClient();
const downloadReq = request.newBuilder()
.uri(uri.create(finalUrl))
.timeout(Java.type('java.time.Duration').ofSeconds(10))
.build();
const response = client.send(downloadReq, bodyHandler.ofByteArray());
const imageBytes = response.body();
if (!imageBytes || imageBytes.length === 0) {
throw new Error('Empty image received');
}
// ============================================
// NAS STORAGE: For history
// ============================================
try {
const timestamp = Date.now();
const filename = `${timestamp}-${eventId}.jpg`;
const filepath = `${nasPath}/${filename}`;
const Files = Java.type('java.nio.file.Files');
const Paths = Java.type('java.nio.file.Paths');
const path = Paths.get(filepath);
Files.write(path, imageBytes);
log(`💾 NAS: ${filename} (${Math.round(imageBytes.length / 1024)}KB)`, 'debug');
// Cleanup: Keep only last 50 files
const File = Java.type('java.io.File');
const nasDir = new File(nasPath);
const files = nasDir.listFiles();
if (files && files.length > 50) {
const fileList = Array.from(files).sort((a, b) => a.lastModified() - b.lastModified());
const toDelete = fileList.slice(0, fileList.length - 50);
toDelete.forEach(file => file.delete());
log(`🧹 NAS cleanup: ${toDelete.length} old files deleted (keeping 50 newest)`, 'debug');
}
} catch (nasError) {
log(`⚠️ NAS storage failed: ${nasError}`, 'warn');
// Not critical - continue with Image Item
}
// ============================================
// IMAGE ITEM: For notification (Round-Robin Buffer)
// ============================================
const base64 = Java.type('java.util.Base64').getEncoder().encodeToString(imageBytes);
const base64String = `data:image/jpeg;base64,${base64}`;
const imageItemName = `Frigate_Snapshot_FrontDoor_${current_buffer}`;
items.getItem(imageItemName).sendCommand(base64String);
// Rotate buffer: 1 → 2 → 3 → 4 → 5 → 1
const usedBuffer = current_buffer;
current_buffer = (current_buffer % 5) + 1;
if (attempt > 1) {
log(`✅ Snapshot ${usedThumbnail ? '(Thumbnail)' : ''} after ${attempt} attempts → Buffer ${usedBuffer} (${Math.round(imageBytes.length / 1024)}KB)`, 'info');
} else {
log(`✅ Snapshot ${usedThumbnail ? '(Thumbnail)' : ''} → Buffer ${usedBuffer} (${Math.round(imageBytes.length / 1024)}KB)`, 'debug');
}
return imageItemName; // Return Item name for notification
} catch (error) {
log(`❌ Download attempt ${attempt}/${MAX_RETRIES} failed: ${error}`, 'warn');
if (attempt < MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
} else {
log(`❌ Snapshot download failed after ${MAX_RETRIES} attempts`, 'error');
return null;
}
}
}
return null;
};
// ============================================================
// FEATURE 2: Latest Frame Download (without Event ID)
// ============================================================
// Use case: Physical doorbell button pressed, but no motion event active
// Gets the most recent frame from the camera
const downloadLatestFrame = async (cameraName = 'frontdoor') => {
const latestFrameUrl = `http://192.168.1.100:5000/api/${cameraName}/latest.jpg`;
const nasPath = '/mnt/nas/snapshots/frontdoor';
try {
log(`📸 Fetching latest frame for ${cameraName}...`, 'debug');
const http = Java.type('java.net.http.HttpClient');
const request = Java.type('java.net.http.HttpRequest');
const uri = Java.type('java.net.URI');
const bodyHandler = Java.type('java.net.http.HttpResponse$BodyHandlers');
const client = http.newHttpClient();
const downloadReq = request.newBuilder()
.uri(uri.create(latestFrameUrl))
.timeout(Java.type('java.time.Duration').ofSeconds(10))
.build();
const response = client.send(downloadReq, bodyHandler.ofByteArray());
const imageBytes = response.body();
if (!imageBytes || imageBytes.length === 0) {
throw new Error('Empty image received');
}
// NAS storage (same as event snapshots)
try {
const timestamp = Date.now();
const filename = `${timestamp}-latest-frame.jpg`;
const filepath = `${nasPath}/${filename}`;
const Files = Java.type('java.nio.file.Files');
const Paths = Java.type('java.nio.file.Paths');
const path = Paths.get(filepath);
Files.write(path, imageBytes);
log(`💾 NAS: ${filename} (${Math.round(imageBytes.length / 1024)}KB)`, 'debug');
// Cleanup
const File = Java.type('java.io.File');
const nasDir = new File(nasPath);
const files = nasDir.listFiles();
if (files && files.length > 50) {
const fileList = Array.from(files).sort((a, b) => a.lastModified() - b.lastModified());
const toDelete = fileList.slice(0, fileList.length - 50);
toDelete.forEach(file => file.delete());
log(`🧹 NAS cleanup: ${toDelete.length} old files deleted`, 'debug');
}
} catch (nasError) {
log(`⚠️ NAS storage failed: ${nasError}`, 'warn');
}
// Image Item (round-robin)
const base64 = Java.type('java.util.Base64').getEncoder().encodeToString(imageBytes);
const base64String = `data:image/jpeg;base64,${base64}`;
const imageItemName = `Frigate_Snapshot_FrontDoor_${current_buffer}`;
items.getItem(imageItemName).sendCommand(base64String);
const usedBuffer = current_buffer;
current_buffer = (current_buffer % 5) + 1;
log(`✅ Latest Frame → Buffer ${usedBuffer} (${Math.round(imageBytes.length / 1024)}KB)`, 'info');
return imageItemName;
} catch (error) {
log(`❌ Latest frame download failed: ${error}`, 'error');
return null;
}
};
// ============================================================
// FEATURE 3: Telegram Photo Delivery
// ============================================================
// Sends Base64 image from Image Item directly to Telegram
// Works alongside openHAB Cloud notifications
const sendTelegramPhoto = async (imageItemName, caption) => {
try {
if (!imageItemName) {
log(`⚠️ No Image Item for Telegram photo`, 'warn');
return false;
}
let sentCount = 0;
let failedCount = 0;
// Get Base64 data from Image Item
const imageData = items.getItem(imageItemName).rawState.toFullString();
// Send to all Telegram recipients with error isolation
const telegramRecipients = [
{ name: 'User1', action: actions.get("telegram", "telegram:telegramBot:user1") },
{ name: 'User2', action: actions.get("telegram", "telegram:telegramBot:user2") }
];
for (const recipient of telegramRecipients) {
if (recipient.action) {
try {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), RECIPIENT_TIMEOUT_MS)
);
const sendPromise = new Promise((resolve) => {
recipient.action.sendTelegramPhoto(imageData, caption);
resolve(true);
});
await Promise.race([sendPromise, timeoutPromise]);
sentCount++;
log(`✅ Telegram photo sent to ${recipient.name}`, 'debug');
} catch (e) {
failedCount++;
log(`⚠️ Telegram photo to ${recipient.name} failed: ${e.message}`, 'warn');
}
} else {
log(`⚠️ Telegram action '${recipient.name}' not available`, 'warn');
}
}
if (sentCount > 0) {
log(`✅ Telegram photo sent to ${sentCount} recipients: ${caption}`, 'info');
if (failedCount > 0) {
log(`⚠️ ${failedCount} recipients failed`, 'warn');
}
return true;
} else {
log(`❌ Telegram photo could not be sent to ANY recipient`, 'error');
return false;
}
} catch (error) {
log(`Error sending Telegram photo: ${error}`, 'error');
return false;
}
};
// Condition check for front door notifications
const checkFrontDoorConditions = () => {
const doorOpen = items.getItem('DoorContact_FrontDoor_State')?.state === 'OPEN';
const timerActive = entrance_door_timer !== null;
const camAuto = items.getItem('Camera_FrontDoor_AutoRecord')?.state === 'ON';
if (timerActive) {
return { allowed: false, reason: '30s cooldown after door state change' };
}
if (doorOpen) {
return { allowed: false, reason: 'Door is open' };
}
if (!camAuto) {
return { allowed: false, reason: 'Auto record is OFF' };
}
return { allowed: true };
};
// ============================================================
// RULE 1: Door State Timer
// ============================================================
// Blocks notifications for 30s after door state changes
rules.JSRule({
name: "Frigate_FrontDoor-01-Timer",
description: "30-second timer after door state change (blocks notifications)",
triggers: [
triggers.ItemStateChangeTrigger('DoorContact_FrontDoor_State')
],
execute: () => {
try {
if (entrance_door_timer) {
clearTimeout(entrance_door_timer);
}
entrance_door_timer = setTimeout(() => {
entrance_door_timer = null;
log("Front door timer expired (30s)", 'debug');
}, 30000);
log("🚪 Door state changed - 30s cooldown started (blocks notifications)", 'info');
} catch (error) {
log(`Error in door timer: ${error}`, 'error');
}
}
});
// ============================================================
// RULE 2: Motion Detection + Cat Doorbell
// ============================================================
// Handles both normal motion detection AND zone-based cat doorbell
rules.JSRule({
name: "Frigate_FrontDoor-02-Snapshot",
description: "Snapshot for motion + zone-based cat doorbell + Telegram photos",
triggers: [
triggers.ItemStateChangeTrigger('frigate_cam_frontdoor_Event_JSON')
],
execute: async () => {
try {
// Parse Event JSON
const eventJsonString = items.getItem('frigate_cam_frontdoor_Event_JSON')?.state?.toString();
if (!eventJsonString || eventJsonString === 'NULL' || eventJsonString === 'UNDEF') {
log("No event JSON available", 'debug');
return;
}
let event;
try {
event = JSON.parse(eventJsonString);
} catch (e) {
log(`JSON parse error: ${e}`, 'error');
return;
}
const eventId = event.after?.id;
const label = event.after?.label;
const eventType = event.type;
const zones = event.after?.current_zones || [];
if (!eventId || !label) {
log("Incomplete event (no ID or label)", 'debug');
return;
}
log(`🔍 JSON-EVENT ${eventId} | Type: ${eventType} | Label: ${label} | Zones: [${zones.join(', ')}]`, 'info');
// ============================================
// BRANCHING: Cat or normal motion?
// ============================================
const isCat = label?.toLowerCase().includes('cat');
if (isCat) {
// ========================================
// CAT DOORBELL LOGIC (Zone-based!)
// ========================================
const beforeZones = event.before?.current_zones || [];
const afterZones = event.after?.current_zones || [];
// Trigger only when cat ENTERS the zone
const enteredCatZone =
!beforeZones.includes("cat_doorbell") &&
afterZones.includes("cat_doorbell");
if (!enteredCatZone) {
log(`🐈 Cat detected, but not in zone 'cat_doorbell' (Event ${eventId})`, 'debug');
return;
}
log(`🐈 Cat entered zone 'cat_doorbell' (Event ${eventId})`, 'info');
// 60s cooldown for cat doorbell
const now = Date.now();
if (now - last_catbell_time < CATBELL_COOLDOWN_MS) {
const secondsSince = Math.round((now - last_catbell_time) / 1000);
log(`🐈 Cat doorbell ignored (cooldown: ${secondsSince}s since last notification)`, 'debug');
return;
}
// Check all cat doorbell conditions
const timer_active = entrance_door_timer !== null;
const door_closed = items.getItem('DoorContact_FrontDoor_State')?.state === 'CLOSED';
const catbell_enabled = items.getItem('CatDoorbell_FrontDoor_Enabled')?.state === 'ON';
if (timer_active) {
log(`🐈 Cat doorbell ignored: 30s cooldown active`, 'debug');
return;
}
if (!door_closed) {
log(`🐈 Cat doorbell ignored: Door open`, 'debug');
return;
}
if (!catbell_enabled) {
log(`🐈 Cat doorbell ignored: Feature disabled`, 'debug');
return;
}
// ALL CONDITIONS MET - Remember timestamp & send notification
last_catbell_time = now;
log(`🐈⬛ CAT DOORBELL! Cat in zone (${eventId})`, 'info');
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 700));
// Download Snapshot → NAS + Image Item
const imageItemName = await downloadSnapshotToBuffer(eventId);
if (!imageItemName) {
log(`⚠️ Cat doorbell: Snapshot download failed (Event ${eventId})`, 'warn');
// Send cat doorbell notification ALWAYS, even without image!
}
// Short pause for Item update & Cloud sync
if (imageItemName) {
await new Promise(resolve => setTimeout(resolve, 300));
}
const nowDate = new Date();
const timestamp = nowDate.toISOString().replace(/[-:T]/g, '').split('.')[0];
const notifTimestamp = `${nowDate.getHours().toString().padStart(2, '0')}:${nowDate.getMinutes().toString().padStart(2, '0')}:${nowDate.getSeconds().toString().padStart(2, '0')}`;
sendNotification(
`Please let me in! ${notifTimestamp}`,
{
title: "🐈⬛ Cat Doorbell",
tag: "Cat Doorbell",
icon: "motion",
mediaUrl: imageItemName,
id: `catbell-${timestamp}`,
recipients: ['user1@example.com', 'user2@example.com'] // Your email addresses
}
);
log(`🐈⬛ Cat doorbell notification sent (${eventId})`, 'info');
// Telegram photo
if (imageItemName) {
await sendTelegramPhoto(imageItemName, `🐈⬛ Cat at front door`);
}
return; // DONE - no regular motion snapshot
}
// ========================================
// NORMAL MOTION (non-cat objects)
// ========================================
const beforeHadSnapshot = event.before?.has_snapshot === true;
const afterHasSnapshot = event.after?.has_snapshot === true;
// Trigger when snapshot becomes available
const snapshotAvailable =
(eventType === "new" && afterHasSnapshot) ||
(!beforeHadSnapshot && afterHasSnapshot);
if (!snapshotAvailable) {
log(`Snapshot not available (Event ${eventId}, Type: ${eventType})`, 'debug');
return;
}
log(`✅ Snapshot available (Event ${eventId}, Type: ${eventType})`, 'debug');
// Deduplication
if (processed_snapshots.has(eventId)) {
log(`Snapshot for event ${eventId} already processed`, 'debug');
return;
}
// Cooldown
const now = Date.now();
if (now - last_snapshot_timestamp < SNAPSHOT_COOLDOWN_MS) {
const elapsed = Math.round((now - last_snapshot_timestamp) / 1000);
log(`Snapshot ignored (cooldown active: ${elapsed}s)`, 'debug');
return;
}
// Conditions
const conditions = checkFrontDoorConditions();
if (!conditions.allowed) {
log(`Event ${eventId} ignored (${conditions.reason})`, 'debug');
return;
}
// Memory leak prevention
if (processed_snapshots.size >= MAX_CACHE_SIZE) {
const cacheArray = Array.from(processed_snapshots);
const toDelete = cacheArray.slice(0, 10);
toDelete.forEach(item => processed_snapshots.delete(item));
log(`⚠️ Snapshot cache full (${MAX_CACHE_SIZE}) - deleting 10 oldest entries`, 'warn');
}
processed_snapshots.add(eventId);
last_snapshot_timestamp = now;
if (cleanup_timer_snapshots) {
clearTimeout(cleanup_timer_snapshots);
cleanup_timer_snapshots = null;
}
cleanup_timer_snapshots = setTimeout(() => {
const oldSize = processed_snapshots.size;
processed_snapshots.clear();
cleanup_timer_snapshots = null;
log(`🧹 Snapshot cache cleared (${oldSize} entries removed)`, 'debug');
}, SNAPSHOT_CACHE_CLEANUP_MS);
log(`📸 "${label}" detected (Event ${eventId}) - cached`, 'info');
// Initial delay
await new Promise(resolve => setTimeout(resolve, 700));
// Download Snapshot → NAS + Image Item
const imageItemName = await downloadSnapshotToBuffer(eventId);
if (!imageItemName) {
log(`⚠️ Snapshot download failed - no notification (Event ${eventId})`, 'warn');
return;
}
// Short pause for Item update & Cloud sync
await new Promise(resolve => setTimeout(resolve, 300));
sendNotification(
`${label} at front door`,
{
title: "Front Door Camera",
tag: "Camera Motion Front Door",
icon: "motion",
mediaUrl: imageItemName,
id: `frontdoor-snapshot-${eventId}`,
recipients: ['user1@example.com', 'user2@example.com'] // Your email addresses
}
);
log(`📧 Snapshot notification sent (${eventId})`, 'info');
// Telegram photo
await sendTelegramPhoto(imageItemName, `📸 ${label} at front door`);
} catch (error) {
log(`Error in snapshot/cat doorbell: ${error}\n${error.stack}`, 'error');
}
}
});
// ============================================================
// RULE 3: Physical Doorbell Button
// ============================================================
// Triggered by physical doorbell button press
// Priority: Latest Frame → Event Snapshot (fallback)
rules.JSRule({
name: "Frigate_FrontDoor-03-Doorbell",
description: "Camera snapshot when doorbell button pressed (Latest Frame with Event Snapshot fallback + Telegram photo)",
triggers: [
triggers.ItemStateChangeTrigger('Doorbell_Virtual', undefined, 'ON')
],
execute: async () => {
try {
log("🔔 Doorbell pressed", 'info');
await new Promise(resolve => setTimeout(resolve, 700));
// PRIORITY: Latest Frame FIRST, Event Snapshot as fallback
let imageItemName = null;
// Attempt 1: Latest Frame (MOST CURRENT image!)
log(`🔔 Loading latest frame (most current image)`, 'debug');
imageItemName = await downloadLatestFrame('frontdoor');
// Fallback: Event Snapshot (if Latest Frame fails)
if (!imageItemName) {
const eventId = items.getItem('frigate_cam_frontdoor_Current_Event_ID')?.state?.toString();
if (eventId && eventId !== 'NULL' && eventId !== 'UNDEF') {
log(`⚠️ Latest frame failed - using event snapshot: ${eventId}`, 'warn');
imageItemName = await downloadSnapshotToBuffer(eventId);
} else {
log(`⚠️ Latest frame failed + no event ID available`, 'warn');
}
}
// Short pause for Cloud sync (only if image available)
if (imageItemName) {
await new Promise(resolve => setTimeout(resolve, 300));
}
// Send notification (TO ALL!)
const nowDate = new Date();
const timestamp = `${nowDate.getHours().toString().padStart(2, '0')}:${nowDate.getMinutes().toString().padStart(2, '0')}:${nowDate.getSeconds().toString().padStart(2, '0')}`;
const uniqueId = nowDate.toISOString().replace(/[-:T.]/g, '').slice(0, 14);
sendNotification(
`Someone is at the front door. ${timestamp}`,
{
title: "🔔 Doorbell",
tag: "Doorbell",
icon: "doorbell",
mediaUrl: imageItemName,
id: `doorbell-ring-${uniqueId}`,
recipients: ['user1@example.com', 'user2@example.com'] // Your email addresses
}
);
log(`🔔 Doorbell notification sent: ${timestamp}`, 'info');
// Telegram photo (only if image available)
if (imageItemName) {
await sendTelegramPhoto(imageItemName, `🔔 Doorbell`);
}
} catch (error) {
log(`Error processing doorbell: ${error}\n${error.stack}`, 'error');
}
}
});
// ============================================================
// STARTUP
// ============================================================
rules.JSRule({
name: "Frigate_FrontDoor-04-Startup",
description: "Module loaded",
triggers: [
triggers.ItemStateChangeTrigger('Openhab_Online', undefined, 'ON'),
triggers.ItemStateChangeTrigger('Rule_Reload', undefined, 'ON')
],
execute: () => {
console.info("✅ [FRIGATE_FRONTDOOR] Front Door Camera loaded");
log("Front Door Camera Module loaded", 'info');
// Cleanup timers
if (entrance_door_timer) {
clearTimeout(entrance_door_timer);
entrance_door_timer = null;
}
if (cleanup_timer_snapshots) {
clearTimeout(cleanup_timer_snapshots);
cleanup_timer_snapshots = null;
}
// Reset tracking variables
processed_snapshots.clear();
last_snapshot_timestamp = 0;
last_catbell_time = 0;
current_buffer = 1;
log("✅ All timers and caches reset", 'debug');
}
});
log('frigate_frontdoor.js loaded', 'info');
How It Works
IMPORTANT: Before using this code, replace all instances of
['user1@example.com', 'user2@example.com']
with your actual email addresses registered with openHAB Cloud (myopenhab.org).
Normal Motion Detection
- Frigate detects motion (person/car/etc.)
- JSON event published via MQTT → openHAB Frigate Binding
- Rule 2 triggers when
has_snapshotbecomes true - Checks conditions:
- Is door closed?
- Is timer inactive?
- Is AutoRecord enabled?
- Downloads snapshot (with retry + thumbnail fallback)
- Saves to NAS (last 50 per camera) + Image Item (round-robin buffer)
- Sends notification via openHAB Cloud (to specified email addresses)
- Sends Telegram photo (Base64 from Image Item)
Cat Doorbell (Zone-Based)
- Frigate detects cat in specific zone (
cat_doorbell) - Rule 2 branches to cat logic
- Additional conditions:
- 60s cooldown
- Door closed
- Feature enabled
- Sends special notification (“Please let me in!”)
Physical Doorbell
- Button pressed (hardware switch → Virtual Item)
- Rule 3 triggers
- Priority strategy:
- Latest Frame first (most current image)
- Event Snapshot fallback (if latest fails)
- Notification with image (or without if both fail)
- Telegram photo
Why This Approach?
Hybrid Storage (NAS + Image Items):
- NAS provides permanent history
- Image Items work with openHAB Cloud (no VPN needed)
- Round-robin buffer prevents race conditions
Latest Frame for Doorbell:
- Doorbell often pressed when motion already stopped
- Latest frame is more current than old event snapshot
- Graceful fallback if latest frame unavailable
Retry Logic:
- Frigate snapshots take time to write
- 3 retries with 1s delay catches “still writing” errors
- Thumbnail fallback ensures we always get something
Error Isolation:
- Telegram send failures don’t block other recipients
- Each recipient has timeout protection (15s)
- NAS failures don’t prevent Image Item storage
Zone-Based Cat Doorbell:
- Only triggers when cat enters specific zone
- Prevents false positives from cat just walking by
- 60s cooldown prevents notification spam
Questions?
Happy to share more details about any part of this setup.
I’m wondering if the Frigate binding was removed from the addon store? Nothing shows up under “frigate” any more.
Same here ![]()
I’ll check. I haven’t removed it, but I need to build a version against the recent OH, so that may be it. Sorry for the radio-silence on this - I am very busy with the day job at the moment. However I am working on a v3 of this binding - more soon.
I’ll update the addon store soon.
Just released v2.3 and updated the Marketplace entry - it should work now.
Just released v2.4 with TLS and authentication support to the Frigate server API. This is backported from the forthcoming v3.x
It’s here! I have just released v3.0 of this binding, with a brand new architecture (thanks to @t2000 for this) and some significant improvements to streaming and ThingActions. PTZ is now directly supported via a ThingAction, and there are some new channels that simplify the use as a detector to trigger rules. Enjoy!
Hi everyone / Hi developer,
I recently upgraded my system to openHAB 5.2.1 and noticed an issue with the MQTT frigateSVR binding.
My System Setup:
-
Hardware: Raspberry Pi
-
OS: openHABian
-
openHAB Version: 5.2.1
The Issue: When I try to add a new Thing manually via the Main UI, the catalog is completely empty. The UI simply displays this message:
“No thing types can be added with this binding.” (See attached screenshot).
Additionally, I noticed these timeout warnings in the openhab.log which might be related to the missing Thing/Channel definitions in OH 5.x:
[WARN ] [core.thing.internal.ThingManagerImpl] - Channel types or config descriptions for thing ‘mqtt:frigateCamera:…’ are missing in the respective registry for more than 120s. In case it does not happen immediately after an upgrade, it should be fixed in the binding.
Could you check if the XML definitions or the ThingTypeProvider need an update to be fully compatible with openHAB 5?
Thanks for your help and your work on this binding!
I’ll see if I can recreate this in the test environment - I have the binding working well here in OH 5.2.1 after re-creating the Things and haven’t seen this behaviour
Are you upgrading from v2.x of this binding or is this a fresh install? If an upgrade, you will almost certainly have to fully delete and recreate your Things, as the architecture is substantially different in v3.01 of this binding.
Hello. (First message here, so primary info is : thanks for your work !)
I have the same behavior:
“No thing types can be added with this binding.
First install of the binding (so no upgrade), openHAB 5.2.0.
"No, I upgraded from 5.2 to 5.2.1, so it is not a fresh installation.
I also noticed another issue: the Frigate binding always disappears from my system whenever I clear the cache, and I have to manually reinstall it every time.
Do you need any specific logs from my side to help investigate this?"
By the way, since we are discussing the binding: Is there any chance to implement an audio return channel (two-way audio) in the future? It would be amazing if the binding could expose the camera as an ‘Audio Sink’ in openHAB. This would open up great possibilities, like intercom functionality or playing TTS (Text-to-Speech) messages directly through the camera’s speaker. Just a feature request/idea!
Ok - can you please just check you are following these steps? I have worked through this on a vanilla install of OH 5.2.1 on the test system and it works.
1: In Things, click on the + sign and select ‘MQTT Binding’. From there, ensure you have an ‘MQTT Broker’ Thing created and online.
2: Then, from Things, click on ‘MQTT Binding’. An option to add ‘frigateSVR Server’ should appear. Click on this to create the Server Thing, add the URL to your Frigate server and select the MQTT Broker thing created in step 1 as the 'Parent Bridge". When saved, the frigate Server Thing should be online.
3: From Things, click on ‘MQTT Binding’. You should then see the ‘frigateSVR Camera’ option appear.You can either click on this to create a Camera Thing manually (selecting the frigateSVR Server Thing from step 2 as the Parent Bridge), or, if you select ‘Scan’, you should see the autodiscovered cameras supported by your Frigate server appear - these then can be added by clicking on each of them
Try the above.
The architecture of the V3 bindings is a set of nested bridges: frigateSVR Cameras are children of a frigateSVR Server, which is itself a bridge and a child of an MQTT Broker.
Interesting idea! You can, of course get the audio from the Frigate streams, but this would have some clear advantages as you point out. I’ll put this on the to-do list.
Not sure why this is happening - I will run some tests.


