Skip to content

Fix ONVIF motion against real cameras, and allow choosing the event - #57

Open
Mo3he wants to merge 2 commits into
skrashevich:hksvfrom
Mo3he:onvif-fold
Open

Mo3he wants to merge 2 commits into
skrashevich:hksvfrom
Mo3he:onvif-fold

Conversation

@Mo3he

@Mo3he Mo3he commented Aug 29, 2026

Copy link
Copy Markdown

I tried motion: onvif against three cameras and it worked on none of
them, each failing for a different reason. This fixes all three. Every
finding here comes from probing the actual hardware, and the whole lot is
now running on my cameras.

Independent of AlexxIT#2130's HKSV changes: different files, no overlap beyond
the config struct and the README.

Transport: 400 on every PullMessages, two separate causes

Axis returns the same subscription URL for every subscription and
identifies them by a SubscriptionId inside ReferenceParameters. Only
Address was kept, so the camera could not tell which subscription was
being polled. Those parameters are now echoed back as SOAP headers, which
is what WS-Addressing requires. Confirmed on the camera: identical
request, 400 without the header, 200 with it.

Reolink rejects a subscription-manager request carrying no wsa:To
and wsa:Action. Both are now sent, with the correct action URI per
operation. Same test: 400 without, 200 with.

Sending both satisfies all three brands, so it is one code path rather
than per-brand handling. Renew and Unsubscribe carry them too. Renewal
fires about 50s into a session, so getting that wrong looks like motion
working for a minute and then stopping.

Parsing: valid events silently dropped

SimpleItem attributes were matched with one pattern requiring Name
before Value. Tapo sends Value first, which is equally valid XML, so
every one of its motion events was discarded with no error at any log
level. Attributes are now read independently of order. A Tapo C200 went
from 0 of 116 events parsed to 116 of 116.

Choosing which event

Only four hardcoded motion topics were recognised, and only IsMotion or
State as the value. A camera's own analytics are usually more accurate
and much less noisy: one tested Axis emitted 137 events in 75 seconds, all
but 3 of them PTZ and hardware status.

onvif_topic selects by substring, and accepts a list so several can
trigger:

homekit:
  driveway:
    motion: onvif
    onvif_url: onvif://user:pass@192.168.1.123
    onvif_topic: ObjectAnalytics/Device1Scenario1   # AXIS Object Analytics
    onvif_topic:                                    # a person, or anything moving
      - MyRuleDetector/PeopleDetect
      - CellMotionDetector

active joins the default item names, which is what Axis application
topics report under, so those need nothing else. onvif_items overrides
the names for anything unusual.

Defaults are unchanged: no topic means the same four motion topics as
before, a single topic can still be written as a plain string, and
ParseMotionEvents is kept as a wrapper over the new ParseEvents.

Verified

Running on 7 cameras across 3 brands. Each trigger type confirmed firing
in production with hold times behaving correctly (30s floor, extending
while activity continues):

  • Axis AOA person detection (2 cameras)
  • Axis hardware PIR, via Sensor/PIR
  • Reolink motion, and its MyRuleDetector/PeopleDetect
  • Tapo cell motion (3 cameras)

Tests use payloads captured from the cameras themselves, including Tapo's
attribute order and a real AOA message.

Two things worth documenting that cost me time

GetEventProperties under-reports. A Reolink advertises 5 topics while
emitting 8, hiding every AI detector, and still sets FixedTopicSet to
true. Subscribing with no filter for a minute is the only reliable way
to enumerate them. Noted in the README.

The ONVIF port is usually not 80 and is not discoverable from an RTSP
URL: Tapo uses 2020 with 80 closed, Reolink 8000. Since auto-discovery
only works when a stream source is itself an onvif:// URL, onvif_url
is effectively required. Also noted.

Not addressed

pkg/onvif/client.go hardcodes http://, so an HTTPS-only camera still
cannot be used. And there is a pre-existing data race on motionActive
in onvif_motion.go between the poll loop and the hold-timer goroutine.
Both left alone to keep this focused; happy to take either in a follow-up.

Mo3he added 2 commits August 29, 2026 21:34
ONVIF motion did not work on any of the three cameras I tested it against,
each for a different reason. All three are fixed here and verified live.

Transport, two separate causes of a 400 on every PullMessages:

- Axis returns the same subscription URL for every subscription and
  identifies them by a SubscriptionId in ReferenceParameters. Those were
  discarded, so the camera could not tell which subscription was being
  polled. They are now echoed back as SOAP headers, as WS-Addressing
  requires.
- Reolink rejects a subscription-manager request that carries no wsa:To
  and wsa:Action. Both are now sent, with the right action URI per
  operation.

Sending both satisfies all three brands, so this is one path rather than
per-brand handling. Renew and Unsubscribe carry them too: renewal happens
about 50s into a session, so getting it wrong looks like motion working
for a minute and then stopping.

Parsing:

- SimpleItem attributes were matched with a single pattern requiring Name
  before Value. Tapo sends Value first, which is equally valid XML, so
  every one of its motion events was silently dropped. Attributes are now
  read independently of order. A Tapo went from 0 of 116 events parsed to
  116 of 116.

Choosing the event:

- Only four hardcoded motion topics were recognised, and only IsMotion or
  State as the value. A camera's own analytics are usually more accurate
  and far less noisy than a pixel-change alarm, so onvif_topic selects a
  topic by substring: an AXIS Object Analytics scenario, or one of the
  per-class detectors a Reolink exposes. "active" joins the default item
  names, which is what the Axis application topics report under, so those
  need no other config. onvif_items overrides the names for anything
  unusual.

Defaults are unchanged: no topic means the same four motion topics as
before, and ParseMotionEvents is kept as a wrapper over ParseEvents.

Tests use payloads captured from the cameras themselves, including Tapo's
attribute order and a real AOA message.
A camera often exposes a dedicated person detector alongside plain motion,
and a doorbell wants both: someone walking up, or anything else moving.
onvif_topic now accepts a list and triggers on any one of them.

A single topic can still be written as a plain string, so existing config
is unaffected.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant