Skip to content

feat: onvif support#28

Merged
lan17 merged 19 commits intomainfrom
lev/onvif
Feb 23, 2026
Merged

feat: onvif support#28
lan17 merged 19 commits intomainfrom
lev/onvif

Conversation

@lan17
Copy link
Copy Markdown
Owner

@lan17 lan17 commented Feb 22, 2026

Summary

Add end-to-end ONVIF support: automatic camera discovery via WS-Discovery, credential-authenticated probing for device info and media profiles, and a guided UI wizard that turns discovered cameras into RTSP-backed camera configs with a few clicks.

Backend — homesec.onvif package

  • Discovery (discovery.py): WS-Discovery client that probes for both Device and NetworkVideoTransmitter ONVIF types separately (WS-Discovery AND-matches within a single probe), merges and deduplicates results. Handles quirky cameras (TP-Link, Hikvision, budget models) that reuse the probe MessageID by enabling relates_to=True. Configurable TTL (default 4) for cross-VLAN multicast.
  • Client (client.py): Async ONVIF camera client wrapping onvif-zeep-async — fetches device info, media profiles, and RTSP stream URIs. Resolves WSDL path from the installed package rather than fragile data_files placement.
  • Service (service.py): Shared orchestration layer used by both the API and CLI. Manages client lifecycle (including best-effort close with bounded timeout), normalizes credentials, and maps exceptions into typed errors (OnvifDiscoverError, OnvifProbeError, OnvifProbeTimeoutError).
  • CLI (cli.py / __main__.py): Standalone python -m homesec.onvif CLI with discover, info, and streams subcommands. Passwords are prompted via getpass when omitted, and host:port syntax is parsed automatically.

API — FastAPI routes

  • POST /api/v1/onvif/discover — triggers a WS-Discovery scan, returns discovered cameras.
  • POST /api/v1/onvif/probe — probes a specific camera with credentials, returns device info, media profiles, and per-profile RTSP stream URIs. Timeout and auth errors mapped to appropriate HTTP status codes (504/502).

UI — ONVIF Discovery Wizard

Three-step wizard integrated into the Cameras page:

  1. Discover — scans the network, lists found cameras with IP/scope info.
  2. Probe — user enters ONVIF credentials + port (inferred from discovery xaddr), fetches device info and available media profiles.
  3. Select Stream — pick a profile, review the RTSP URI, name the camera, and create it as an RTSP-backed camera config.

State managed via useReducer with typed actions. Presentation helpers handle camera name derivation, port inference from xaddrs, and credential injection into RTSP URIs.

Tests

  • Backend: 1,383 lines of test coverage across discovery, client, service, CLI, and API routes. Tests mock WS-Discovery and ONVIF internals to validate probe flows, error handling, timeout behavior, credential normalization, and CLI argument parsing.
  • Frontend: Wizard integration tests (330 lines) covering the full discover → probe → create flow, plus unit tests for the API client, parsing utilities, and presentation helpers.

Test plan

  • Run pytest — all new and existing backend tests pass
  • Run npm test in ui/ — all new and existing frontend tests pass
  • Manual: run python -m homesec.onvif discover on a network with ONVIF cameras and verify cameras are found
  • Manual: open the UI, click "ONVIF Discovery", complete the wizard end-to-end to create a camera

lan17 and others added 13 commits February 21, 2026 22:50
onvif-zeep hasn't been updated since 2018. Switch to the actively
maintained onvif-zeep-async (v4.0.4) and make the client fully async,
aligning with the project's async-first architecture. Also fix the
WS-Discovery logging suppression to target the correct logger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… discovery

Many cameras (TP-Link, Hikvision, budget models) reuse the probe's
MessageID in responses, causing WSDiscovery to silently drop them as
duplicates. Enable relates_to=True to accept these replies.

Also probe for both Device and NetworkVideoTransmitter ONVIF types
(some cameras only advertise one), and set TTL=4 so multicast probes
can cross VLANs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs were causing cameras to be missed:

1. WS-Discovery Probe type matching is AND, not OR. Passing
   [Device, NVT] in a single probe only found cameras advertising
   both types. Now we send separate probes for each type and merge
   results, catching cameras that only advertise one.

2. The retry loop broke early as soon as any camera was found on
   the first attempt, missing slow-responding cameras. All attempt
   rounds now always run; results are deduplicated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make the password parameter optional in info/streams CLI commands.
When omitted, securely prompts via getpass so credentials don't
leak into shell history or process listings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssword

Three fixes:
- Resolve WSDL directory from inside the onvif package (onvif/wsdl/)
  instead of relying on data_files placement (site-packages/wsdl/)
  which is unreliable across installers and platforms.
- Add close() to OnvifCameraClient and call it in CLI finally blocks
  to prevent "Unclosed client session" errors from aiohttp.
- Make password optional in info/streams commands; prompt securely
  via getpass when omitted so credentials don't leak to shell history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parse 'host:port' syntax in the ip argument for info/streams
  commands so '192.168.0.183:2020' works without needing --port.
  Previously the port was appended again, producing an invalid
  address like '192.168.0.183:2020:80'.
- Call ONVIFCamera.close() which properly closes all service
  transports and the snapshot client, fixing "Unclosed client
  session" errors from aiohttp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These methods are coroutines in onvif-zeep-async (unlike the sync
onvif-zeep) and must be awaited. Without await they returned a
coroutine object instead of the service, causing 'coroutine object
has no attribute GetDeviceInformation'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 90.01883% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.49%. Comparing base (a085c23) to head (e0874f0).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/homesec/onvif/cli.py 74.28% 27 Missing ⚠️
src/homesec/onvif/client.py 86.55% 16 Missing ⚠️
src/homesec/onvif/__main__.py 0.00% 4 Missing ⚠️
src/homesec/onvif/discovery.py 97.02% 3 Missing ⚠️
src/homesec/onvif/service.py 98.27% 2 Missing ⚠️
src/homesec/api/routes/onvif.py 98.70% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #28      +/-   ##
==========================================
+ Coverage   82.01%   82.49%   +0.48%     
==========================================
  Files          94      101       +7     
  Lines        8177     8707     +530     
==========================================
+ Hits         6706     7183     +477     
- Misses       1471     1524      +53     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lan17 lan17 changed the title Lev/onvif feat: onvif support Feb 23, 2026
@lan17 lan17 marked this pull request as ready for review February 23, 2026 23:45
@lan17 lan17 merged commit d34f8ac into main Feb 23, 2026
5 checks passed
@lan17 lan17 deleted the lev/onvif branch February 23, 2026 23:45
lan17 added a commit that referenced this pull request Feb 23, 2026
Add end-to-end ONVIF support: automatic camera discovery via
WS-Discovery, credential-authenticated probing for device info and media
profiles, and a guided UI wizard that turns discovered cameras into
RTSP-backed camera configs with a few clicks.

- **Discovery** (`discovery.py`): WS-Discovery client that probes for
both `Device` and `NetworkVideoTransmitter` ONVIF types separately
(WS-Discovery AND-matches within a single probe), merges and
deduplicates results. Handles quirky cameras (TP-Link, Hikvision, budget
models) that reuse the probe MessageID by enabling `relates_to=True`.
Configurable TTL (default 4) for cross-VLAN multicast.
- **Client** (`client.py`): Async ONVIF camera client wrapping
`onvif-zeep-async` — fetches device info, media profiles, and RTSP
stream URIs. Resolves WSDL path from the installed package rather than
fragile `data_files` placement.
- **Service** (`service.py`): Shared orchestration layer used by both
the API and CLI. Manages client lifecycle (including best-effort close
with bounded timeout), normalizes credentials, and maps exceptions into
typed errors (`OnvifDiscoverError`, `OnvifProbeError`,
`OnvifProbeTimeoutError`).
- **CLI** (`cli.py` / `__main__.py`): Standalone `python -m
homesec.onvif` CLI with `discover`, `info`, and `streams` subcommands.
Passwords are prompted via `getpass` when omitted, and `host:port`
syntax is parsed automatically.

- `POST /api/v1/onvif/discover` — triggers a WS-Discovery scan, returns
discovered cameras.
- `POST /api/v1/onvif/probe` — probes a specific camera with
credentials, returns device info, media profiles, and per-profile RTSP
stream URIs. Timeout and auth errors mapped to appropriate HTTP status
codes (504/502).

Three-step wizard integrated into the Cameras page:

1. **Discover** — scans the network, lists found cameras with IP/scope
info.
2. **Probe** — user enters ONVIF credentials + port (inferred from
discovery xaddr), fetches device info and available media profiles.
3. **Select Stream** — pick a profile, review the RTSP URI, name the
camera, and create it as an RTSP-backed camera config.

State managed via `useReducer` with typed actions. Presentation helpers
handle camera name derivation, port inference from xaddrs, and
credential injection into RTSP URIs.

- **Backend**: 1,383 lines of test coverage across discovery, client,
service, CLI, and API routes. Tests mock WS-Discovery and ONVIF
internals to validate probe flows, error handling, timeout behavior,
credential normalization, and CLI argument parsing.
- **Frontend**: Wizard integration tests (330 lines) covering the full
discover → probe → create flow, plus unit tests for the API client,
parsing utilities, and presentation helpers.

- [ ] Run `pytest` — all new and existing backend tests pass
- [ ] Run `npm test` in `ui/` — all new and existing frontend tests pass
- [ ] Manual: run `python -m homesec.onvif discover` on a network with
ONVIF cameras and verify cameras are found
- [ ] Manual: open the UI, click "ONVIF Discovery", complete the wizard
end-to-end to create a camera

---------
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