I run openHAB 4.x at home. Two Imou cameras on the front door and garage—great in the Imou Life app, invisible to my Rules. I spent a weekend on the ONVIF binding. One stream worked over Ethernet. The Wi-Fi camera kept going OFFLINE in the Thing status, and motion never showed up as an Item I could use.
I checked the Imou Open Platform docs. They ship an official component for Home Assistant, not openHAB (resource download lists only the HA zip). Fair enough. The platform does document a cloud-to-cloud HTTP API for device list, live URLs, alarms, and PTZ (application development guide)—so I put a tiny bridge on a Raspberry Pi and fed openHAB over MQTT. Took one evening. No port forwarding for alarms.
This is what I actually run. Not vendor-supported openHAB glue—just openHAB Things/Items/Rules on top of their public API.
What you’re building
Imou cloud --HTTP callback--> bridge (Node on LAN) --MQTT--> openHAB MQTT binding
|
+-- optional: poll device online status
-
Alarms / motion: push via
setMessageCallback→ bridge → MQTT (don’t poll on a timer if you can avoid it) -
Online status: bridge polls
listDeviceDetailsByPageevery few minutes, publishesonline/offline -
Live video: openHAB isn’t a great HLS player; I use the app for live view and openHAB for events + automations. LAN folks can still add ONVIF/RTSP separately.
Before you start
-
Devices already in Imou Life (same account you’ll use on the open platform)
-
openHAB with MQTT binding installed
-
A always-on box for the bridge (Pi, NAS, whatever runs Node 18+)
-
Developer console open in a browser
Register at [Cloud Video And AIoT Services - Imou Open Platform), create an app, copy AppId and AppSecret. Check My Resources for API quota—you’ll consume some on live URL tests.
Pick your API host from the domain list. International examples:
| Column 1 | Column 2 |
|---|---|
| Region | Host |
| APAC | openapi-sg.easy4ip.com |
| Americas | openapi-or.easy4ip.com |
| EU | openapi-fk.easy4ip.com |
Wrong region = empty device list. I did that once; thought my account was broken.
Step 1 — Minimal bridge (alarms → MQTT)
appSecret stays on the bridge machine only—never in openHAB .items files.
// bridge.js — run with: node bridge.js
const crypto = require('crypto');
const express = require('express');
const mqtt = require('mqtt');
const API = 'https://openapi-sg.easy4ip.com'; // change to your region
const APP_ID = process.env.IMOU_APP_ID;
const APP_SECRET = process.env.IMOU_APP_SECRET;
const DEVICE_ID = process.env.IMOU_DEVICE_ID;
const mqttClient = mqtt.connect(process.env.MQTT_URL || 'mqtt://127.0.0.1:1883');
let token = { value: null, expires: 0 };
async function getToken() {
if (Date.now() < token.expires - 60_000) return token.value;
const r = await fetch(`${API}/openapi/accessToken`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appId: APP_ID, appSecret: APP_SECRET }),
});
const j = await r.json();
token = { value: j.data.accessToken, expires: Date.now() + j.data.expireTime * 1000 };
return token.value;
}
function sign() {
const time = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(8).toString('hex');
const sign = crypto.createHash('md5')
.update(`time:${time},nonce:${nonce},appSecret:${APP_SECRET}`).digest('hex');
return { time, nonce, sign };
}
async function api(path, body = {}) {
const accessToken = await getToken();
const { time, nonce, sign } = sign();
const r = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', accessToken, time: String(time), nonce, sign },
body: JSON.stringify({ appId: APP_ID, ...body }),
});
const j = await r.json();
if (j.code === 'SN1005') throw new Error('nonce reused');
return j;
}
// Register alarm callback once at startup
async function registerCallback() {
await api('/openapi/setMessageCallback', {
callbackUrl: process.env.CALLBACK_URL, // e.g. https://your-host/imou/callback
alarmTypes: ['motion', 'human', 'offline'],
});
}
const app = express();
app.use(express.json());
app.post('/imou/callback', (req, res) => {
res.status(200).send('ok'); // cloud stops retrying
const p = req.body;
const topic = `imou/alarm/${p.deviceId || DEVICE_ID}`;
mqttClient.publish(topic, JSON.stringify(p), { qos: 1 });
if (p.type === 'offline' || p.type === 'online') {
mqttClient.publish(`imou/status/${p.deviceId || DEVICE_ID}`, p.type, { qos: 1, retain: true });
}
});
app.listen(3000, () => registerCallback().catch(console.error));
Expose /imou/callback with HTTPS (reverse proxy, Cloudflare tunnel, whatever you already use). The platform needs to reach you for push alarms.
Step 2 — MQTT Thing in openHAB
imou/things/mqtt.imou.things:
Bridge mqtt:broker:local "Local MQTT" [ host="192.168.1.10", port=1883 ] {
Thing topic porch "Porch camera" @ "MQTT" {
Channels:
Type string : alarm_type [
stateTopic="imou/alarm/DEVICE_ID_HERE",
transformation="JSONPATH",
transformationPattern="$.type"
]
Type string : alarm_raw [
stateTopic="imou/alarm/DEVICE_ID_HERE"
]
Type string : online [
stateTopic="imou/status/DEVICE_ID_HERE"
]
}
}
Replace DEVICE_ID_HERE with the serial from the Imou app or from listDeviceDetailsByPage.
Step 3 — Items
imou.items:
String Porch_AlarmType "Alarm type [%s]" { channel="mqtt:topic:porch:alarm_type" }
String Porch_AlarmRaw "Last alarm JSON" { channel="mqtt:topic:porch:alarm_raw" }
String Porch_Online "Cloud status [%s]" { channel="mqtt:topic:porch:online" }
Switch Porch_Light "Porch light" // your Z-Wave / Hue / whatever
(Home Assistant folks would call these “entities”. In openHAB they’re Items linked to Channels on a Thing.)
Step 4 — Rule — motion at night
JavaScript (openHAB 3/4):
// imou_porch_motion.js
rules.JSRule({
name: "Porch motion at night",
triggers: [triggers.ItemStateUpdateTrigger("Porch_AlarmType")],
execute: (event) => {
if (event.itemState.toString() !== "motion" && event.itemState.toString() !== "human") return;
const sun = items.get("Sun_Elevation"); // or use Astro binding
if (sun && sun.stateAsQuantity.doubleValue > 0) return; // daytime, skip
actions.sendCommand("Porch_Light", "ON");
}
});
DSL if you prefer:
rule "Porch motion at night"
when
Item Porch_AlarmType changed
then
if (Porch_AlarmType.state.toString != "motion" && Porch_AlarmType.state.toString != "human") return;
if (Sun_Elevation.state as Number > 0) return;
Porch_Light.sendCommand(ON)
end
Step 5 — Device list (find your DEVICE_ID)
The doc says use paginated device query—not the old flat list API. From the bridge:
const list = await api('/openapi/listDeviceDetailsByPage', { page: 1, pageSize: 50 });
console.log(list.data.deviceList);
Copy deviceId from the JSON into your MQTT topics.
ONVIF vs cloud API — when to use which
| Column 1 | Column 2 | Column 3 |
|---|---|---|
| Approach | Good for | openHAB piece |
| ONVIF / RTSP (LAN) | Local stream, low latency on wired cameras | ONVIF binding or FFmpeg → RTSP URL in Item |
| Cloud OpenAPI + MQTT | Motion/human/offline while you’re away; no router holes | This post |
| HTTP binding only | Polling online status without a bridge script | Possible but dull on API quota |
I kept ONVIF on the wired garage cam and MQTT alarms on both. openHAB doesn’t care—they’re different Items.
Pitfalls (learned the hard way)
Thing ONLINE, Items NULL. MQTT bridge shows connected but Items stay NULL until the first message. Don’t debug Rules on boot—trigger a test motion first.
JSONPATH on alarm payload. Vendor JSON gained extra fields last year; $.type still works but log Porch_AlarmRaw when debugging. If JSONPATH fails silently, the Item stops updating and the Rule never fires.
Callback not HTTP 200 fast enough. We missed a week of events because the handler did SQLite insert before res.send. Cloud retried; we got duplicates later. Answer 200 in milliseconds, process after.
SN1005. Reused nonce across parallel api() calls. One nonce per request.
Live URL in a String Item. bindDeviceLive URLs expire (~10 min). Refresh on a schedule if you insist on embedding video in Basic UI—otherwise you’ll swear the binding is broken.
If you’re on openHAB only and want less scripting: the bridge is ~80 lines; the openHAB side is standard MQTT. Post your openHAB version and MQTT binding version if Items stay NULL—half the time it’s a typo in stateTopic.