frigate/frigate/test/test_motion_source_validator.py
Dmitry Ilyin 53044a475a Native ONVIF cell-motion ingest
Adds a per-camera ONVIF subscriber that lets cameras with native
hardware motion detection (e.g. OpenIPC firmware for HiSilicon,
Ingenic and SigmaStar SoCs; many ONVIF Profile-M devices) replace
Frigate's per-frame CPU motion analysis. Two standard ONVIF
transports are consumed in parallel:

- WS-BaseNotification PullPoint for the binary motion state
  (tns1:RuleEngine/CellMotionDetector/Motion IsMotion=true|false,
   with tns1:VideoSource/MotionAlarm State=true|false accepted as
   a fallback for cameras that only publish the legacy topic).
- RTSP analytics metadata stream (application/vnd.onvif.metadata)
  for the per-frame cell grid (tt:MotionInCells, base64 + PackBits
  bit-packed bitmap). Cell layout is discovered once at startup via
  AnalyticsService.GetAnalyticsModules and the camera's CellLayout
  transformation is used to map cells to detect-frame pixel
  rectangles via connected-components.

New config:
  onvif.events.{enabled, subscription_timeout, use_metadata_stream}
  motion.source: internal (default) | onvif

When motion.source: onvif, ImprovedMotionDetector is skipped and
motion_boxes come from the camera. Internal motion remains the
default; the new path is fully opt-in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:39:22 +03:00

65 lines
2.1 KiB
Python

"""Validator tests for `motion.source=onvif` interactions with
`onvif.events.enabled` and `motion.enabled`. Exercises `verify_motion_and_detect`
directly so we don't need the full FrigateConfig path (which mounts /config)."""
import unittest
from frigate.config.config import verify_motion_and_detect
class _Dummy:
"""Light shim for the nested config attributes the validator reads."""
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def _camera(*, name, detect, motion, onvif):
return _Dummy(name=name, detect=detect, motion=motion, onvif=onvif)
class TestVerifyMotionAndDetect(unittest.TestCase):
def test_internal_motion_with_detect_passes(self):
cam = _camera(
name="c",
detect=_Dummy(enabled=True),
motion=_Dummy(enabled=True, source="internal"),
onvif=_Dummy(events=_Dummy(enabled=False)),
)
# No exception.
self.assertIsNone(verify_motion_and_detect(cam))
def test_detect_with_motion_disabled_rejected(self):
cam = _camera(
name="c",
detect=_Dummy(enabled=True),
motion=_Dummy(enabled=False, source="internal"),
onvif=_Dummy(events=_Dummy(enabled=False)),
)
with self.assertRaisesRegex(ValueError, "object detection requires motion"):
verify_motion_and_detect(cam)
def test_source_onvif_requires_events_enabled(self):
cam = _camera(
name="c",
detect=_Dummy(enabled=True),
motion=_Dummy(enabled=False, source="onvif"),
onvif=_Dummy(events=_Dummy(enabled=False)),
)
with self.assertRaisesRegex(ValueError, "onvif.events.enabled is false"):
verify_motion_and_detect(cam)
def test_source_onvif_with_events_passes_even_with_motion_disabled(self):
cam = _camera(
name="c",
detect=_Dummy(enabled=True),
motion=_Dummy(enabled=False, source="onvif"),
onvif=_Dummy(events=_Dummy(enabled=True)),
)
self.assertIsNone(verify_motion_and_detect(cam))
if __name__ == "__main__":
unittest.main()