From 578985c46b3bfa465212ff295dbd176ddef4b0ba Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sat, 30 May 2026 18:40:34 +0300 Subject: [PATCH] dvrip: add Detect (motion detection) config wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thin wrappers `get_detect_info()` / `set_detect_info(data)` in both the sync (`dvrip.py`) and async (`asyncio_dvrip.py`) clients for the top-level `Detect` config path — per-channel MotionDetect / HumanDetection (and, on multi-channel NVRs, BlindDetect / LossDetect). The library already supported this via the generic `get_info("Detect")` / `set_info("Detect", …)`, but the path and payload shape were undocumented. README now carries a worked example showing the per-channel-array schema and the sparse-merge semantics (fields omitted from a SET payload retain their current value), plus a caveat that AlarmInfo push behaviour is firmware-dependent — some XM builds emit events to the existing TCP session, others only to a separately-configured `AlarmServer`, and some do neither. Verified on a live XM IPC unit (single-channel, hostname IVG85HG50PYA-S-2): the canonical OpenIPC "turn motion detection on" snippet returns `Ret: 100`, round-trip GET shows the change, sparse merge works (`{"MotionDetect":[{"Level":4}]}` flips only `Level`). Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 37 +++++++++++++++++++++++++++++++++++++ asyncio_dvrip.py | 8 ++++++++ connect.py | 7 +++++++ dvrip.py | 8 ++++++++ 4 files changed, 60 insertions(+) diff --git a/README.md b/README.md index 143c49a..31f6aed 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,43 @@ cloudEnabled = False cam.set_info("NetWork.Nat", { "NatEnable" : cloudEnabled }) ``` +## Motion detection + +Xiongmai cameras typically do **not** expose ONVIF `AnalyticsService`, so +motion detection cannot be configured through ONVIF. On some firmwares it +is also off by default. Configure it directly over the native protocol via +the `Detect` config path: + +```python +# Inspect current config (per-channel arrays) +print(cam.get_detect_info()) +# {'HumanDetection': [{'Enable': False, ...}], +# 'MotionDetect': [{'Enable': True, +# 'EventHandler': {'RecordEnable': True, +# 'AlarmOutEnable': False, ...}, +# 'Level': 5}]} + +# Enable motion detection at sensitivity 5 with event-triggered recording. +# Sparse payloads are merged — fields you omit keep their current values. +cam.set_detect_info({ + "MotionDetect": [{"Enable": True, + "EventHandler": {"RecordEnable": True}, + "Level": 5}], + "HumanDetection": [{"Enable": False}], +}) +``` + +The equivalent low-level call is `cam.set_info("Detect", ...)` — both +shapes work; the wrapper just documents the path. + +Receiving the events is a separate problem. Some XM firmwares push +`AlarmInfo` packets over the existing TCP session, so registering +`setAlarm()` + `alarmStart()` is enough; on others the camera only emits +to a separately-configured alarm server (see `AlarmServer.py` and the +`EventHandler.AlarmInfo` / `MsgtoNetEnable` fields). If `setAlarm()` +silently produces no callbacks even while recordings show motion is +detected, the camera is likely in the latter group. + ## Add user and change password ```python diff --git a/asyncio_dvrip.py b/asyncio_dvrip.py index 4060404..edc3b0e 100644 --- a/asyncio_dvrip.py +++ b/asyncio_dvrip.py @@ -580,6 +580,14 @@ class DVRIPCam(object): code = 1042 return await self.get_command("Simplify.Encode", code) + async def get_detect_info(self): + """Read 'Detect' config: per-channel MotionDetect / HumanDetection / etc.""" + return await self.get_info("Detect") + + async def set_detect_info(self, data): + """Update 'Detect' config. Sparse payloads are merged with current state.""" + return await self.set_info("Detect", data) + async def recv_json(self, buf=bytearray()): p = compile(b".*({.*})") diff --git a/connect.py b/connect.py index e6e7b79..84367af 100644 --- a/connect.py +++ b/connect.py @@ -27,6 +27,13 @@ info["OSDInfo"][0]["OSDInfoWidget"]["PreviewBlend"] = True # info["OSDInfo"][0]["OSDInfoWidget"]["RelativePos"] = [6144,6144,8192,8192] cam.set_info("fVideo.OSDInfo", info) # enc_info = cam.get_info("Simplify.Encode") +# Motion detection: turn it on and route events into recording. +# cam.set_detect_info({ +# "MotionDetect": [{"Enable": True, +# "EventHandler": {"RecordEnable": True}, +# "Level": 5}], +# "HumanDetection": [{"Enable": False}], +# }) # Alarm example def alarm(content, ids): print(content) diff --git a/dvrip.py b/dvrip.py index a3c5114..e9007e2 100644 --- a/dvrip.py +++ b/dvrip.py @@ -703,6 +703,14 @@ class DVRIPCam(object): code = 1042 return self.get_command("Simplify.Encode", code) + def get_detect_info(self): + """Read 'Detect' config: per-channel MotionDetect / HumanDetection / etc.""" + return self.get_info("Detect") + + def set_detect_info(self, data): + """Update 'Detect' config. Sparse payloads are merged with current state.""" + return self.set_info("Detect", data) + def recv_json(self, buf=bytearray()): p = compile(b".*({.*})")