From 685b65c35ee683c55dfc115ab7164d74ad4e5894 Mon Sep 17 00:00:00 2001 From: "T. Tradesman" <184814242+ttradesman@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:12:50 +0200 Subject: [PATCH 1/3] fix(event/stream): classify AXIS VMD 4 topics as motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The topic table covered the AXIS Guard suite (MotionGuard, FenceGuard, LoiteringGuard) but not VMD — the stock motion app shipped on every AXIS camera, and the one an installer configures before reaching for a paid Guard product. VMD publishes on tnsaxis:CameraApplicationPlatform/VMD/CameraProfile , which fell through every rule to KindUnknown. Consumers that key recording off KindMotion therefore never recorded, and because an unrecognised Kind is normally discarded at debug level the failure presents as silence rather than an error. Found on a site where an AXIS camera with VMD and a motion recording trigger produced no recordings across a 9h41m window while its ONVIF subscription stayed healthy throughout. --- event/stream/topics.go | 8 +++++--- event/stream/topics_test.go | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/event/stream/topics.go b/event/stream/topics.go index 9071dad..552c132 100644 --- a/event/stream/topics.go +++ b/event/stream/topics.go @@ -79,10 +79,12 @@ var topicRules = []struct { // https://developer.axis.com/vapix/network-video/event-and-action-services/ {"MotionRegionDetector/Motion", KindMotion}, - // AXIS Guard suite — vendor analytics apps with CameraProfile - // suffixes. Treated as motion so they can drive motion-triggered - // recording on cameras using these apps instead of basic VMD. + // AXIS ACAP motion apps with CameraProfile suffixes. VMD is + // the stock app shipped on the camera; the Guard suite are the + // paid analytics products. Prefix-match because of the suffix. + // https://developer.axis.com/vapix/applications/vmd4 // https://developer.axis.com/vapix/applications/motion-guard + {"CameraApplicationPlatform/VMD/", KindMotion}, {"CameraApplicationPlatform/MotionGuard/", KindMotion}, {"CameraApplicationPlatform/FenceGuard/", KindMotion}, {"CameraApplicationPlatform/LoiteringGuard/", KindMotion}, diff --git a/event/stream/topics_test.go b/event/stream/topics_test.go index 2bc2a34..617bf5b 100644 --- a/event/stream/topics_test.go +++ b/event/stream/topics_test.go @@ -25,6 +25,12 @@ func TestClassifyTopic(t *testing.T) { {"axis_fence_guard", "tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1ProfileANY", KindMotion}, {"axis_loitering_guard", "tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1ProfileANY", KindMotion}, + // AXIS VMD 4 — the stock motion app, and the one an installer + // reaches for before any Guard product. The profile suffix + // varies with the configured VMD profile. + {"axis_vmd4_profile_any", "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", KindMotion}, + {"axis_vmd4_profile_numbered", "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", KindMotion}, + // --- Tampering -------------------------------------------------- {"tamper_detector", "tns1:RuleEngine/TamperDetector/Tamper", KindTampering}, From 513c0a8473e3e40f955e645c3dbcb4eba9faae45 Mon Sep 17 00:00:00 2001 From: "T. Tradesman" <184814242+ttradesman@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:52:43 +0200 Subject: [PATCH 2/3] fix(event/stream): classify AXIS VMD 3 topics as motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VMD 3 is the firmware-builtin motion rule, published as tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_. It predates the VMD 4 ACAP and lives under RuleEngine rather than CameraApplicationPlatform, so the VMD 4 rule added in the previous commit does not cover it. Confirmed against a deployed AXIS camera: with debug logging on, its event stream emits vmd3_video_1 on every motion trigger, and every one was discarded as KindUnknown. The VMD 4 rule stays — both generations are in the field. --- event/stream/topics.go | 9 ++++++++- event/stream/topics_test.go | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/event/stream/topics.go b/event/stream/topics.go index 552c132..1345233 100644 --- a/event/stream/topics.go +++ b/event/stream/topics.go @@ -79,7 +79,14 @@ var topicRules = []struct { // https://developer.axis.com/vapix/network-video/event-and-action-services/ {"MotionRegionDetector/Motion", KindMotion}, - // AXIS ACAP motion apps with CameraProfile suffixes. VMD is + // tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_ — AXIS VMD 3, the + // firmware-builtin predecessor of the VMD 4 ACAP. It lives under + // RuleEngine, not CameraApplicationPlatform, so the VMD rule below + // does not cover it. Still shipping on deployed cameras. + // https://developer.axis.com/vapix/network-video/event-and-action-services/ + {"RuleEngine/VMD3/", KindMotion}, + + // AXIS ACAP motion apps with CameraProfile suffixes. VMD 4 is // the stock app shipped on the camera; the Guard suite are the // paid analytics products. Prefix-match because of the suffix. // https://developer.axis.com/vapix/applications/vmd4 diff --git a/event/stream/topics_test.go b/event/stream/topics_test.go index 617bf5b..8e12e26 100644 --- a/event/stream/topics_test.go +++ b/event/stream/topics_test.go @@ -31,6 +31,12 @@ func TestClassifyTopic(t *testing.T) { {"axis_vmd4_profile_any", "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", KindMotion}, {"axis_vmd4_profile_numbered", "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", KindMotion}, + // AXIS VMD 3 — the firmware-builtin predecessor, published under + // RuleEngine rather than CameraApplicationPlatform. Still shipping + // on deployed cameras; observed on site-07 camera-10. + {"axis_vmd3_video_1", "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1", KindMotion}, + {"axis_vmd3_video_2", "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_2", KindMotion}, + // --- Tampering -------------------------------------------------- {"tamper_detector", "tns1:RuleEngine/TamperDetector/Tamper", KindTampering}, From bc9bab3de0378606a9f8c1bd2fd48edc88beb51d Mon Sep 17 00:00:00 2001 From: "T. Tradesman" <184814242+ttradesman@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:25:46 +0200 Subject: [PATCH 3/3] test(event/stream): guard the AXIS VMD needles against overmatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both VMD rules shipped with positive cases only. The file already keeps a "Substring guards" section because Classify matches with strings.Contains, so a needle that is a prefix of a sibling name silently captures it — the same reason MyRuleDetector's sub-rules are whitelisted individually. Pin the two properties the needles rely on: the trailing slash makes them match a whole path segment, and VMD3 is scoped to RuleEngine. Verified by dropping each from the rule and watching these fail. Also drops a site-specific note from the VMD 3 comment — where it was first seen is not something an upstream reader can act on. --- event/stream/topics_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/event/stream/topics_test.go b/event/stream/topics_test.go index 8e12e26..71a06f4 100644 --- a/event/stream/topics_test.go +++ b/event/stream/topics_test.go @@ -32,8 +32,8 @@ func TestClassifyTopic(t *testing.T) { {"axis_vmd4_profile_numbered", "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", KindMotion}, // AXIS VMD 3 — the firmware-builtin predecessor, published under - // RuleEngine rather than CameraApplicationPlatform. Still shipping - // on deployed cameras; observed on site-07 camera-10. + // RuleEngine rather than CameraApplicationPlatform. Still + // shipping on deployed cameras. {"axis_vmd3_video_1", "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1", KindMotion}, {"axis_vmd3_video_2", "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_2", KindMotion}, @@ -100,6 +100,16 @@ func TestClassifyTopic(t *testing.T) { {"relay_failure_not_digital_output", "tns1:Device/HardwareFailure/RelayFailure", KindUnknown}, {"digital_input_config_not_digital_input", "tns1:Device/IO/DigitalInputConfiguration", KindUnknown}, {"tamper_detector_log_not_tampering", "tns1:Device/Diagnostics/TamperDetectorLog", KindUnknown}, + + // The two AXIS VMD needles carry a trailing slash so they match a + // whole path segment. Without it, any sibling app or rule whose + // name merely starts with VMD / VMD3 would classify as motion and + // drive recording. + {"vmd_statistics_app_not_motion", "tnsaxis:CameraApplicationPlatform/VMDStatistics/Camera1", KindUnknown}, + {"vmd3_config_rule_not_motion", "tns1:RuleEngine/tnsaxis:VMD3Config/Changed", KindUnknown}, + // VMD3 is scoped to RuleEngine; the same name under another + // container is a different thing. + {"vmd3_outside_rule_engine_not_motion", "tnsaxis:Storage/VMD3/Status", KindUnknown}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) {