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.


