Metadata-Version: 2.4
Name: onvif-scanner
Version: 0.2.0
Summary: Library + CLI to scan ONVIF cameras on local network via WS-Discovery
Author-email: Riki Handoyo <jawakeror@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/jawak/onvif-scanner
Project-URL: Repository, https://github.com/jawak/onvif-scanner
Project-URL: Issues, https://github.com/jawak/onvif-scanner/issues
Project-URL: Changelog, https://github.com/jawak/onvif-scanner/releases
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: System :: Networking :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: onvif-zeep>=0.2.12
Requires-Dist: wsdiscovery>=0.1.0
Provides-Extra: cli
Requires-Dist: click>=8.1; extra == "cli"
Requires-Dist: rich>=13.0; extra == "cli"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: click>=8.1; extra == "dev"
Requires-Dist: rich>=13.0; extra == "dev"
Dynamic: license-file

# ONVIF Scanner

[![PyPI version](https://img.shields.io/pypi/v/onvif-scanner.svg)](https://pypi.org/project/onvif-scanner/)
[![Python versions](https://img.shields.io/pypi/pyversions/onvif-scanner.svg)](https://pypi.org/project/onvif-scanner/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![CI](https://github.com/jawak/onvif-scanner/actions/workflows/publish.yml/badge.svg)](https://github.com/jawak/onvif-scanner/actions/workflows/publish.yml)

A simple CLI tool to scan for ONVIF cameras on the local network. It uses
WS-Discovery (UDP multicast) to discover devices, then queries the ONVIF
Device Service to retrieve: manufacturer, model, serial number, firmware, and
the **RTSP URL**.

## Features

- **WS-Discovery multicast** on the local subnet (automatic, no manual IP input)
- **Device query** — GetDeviceInformation + GetStreamUri (RTSP URL)
- Output as a **table** (pretty, default) or **JSON** (stdout / file)
- `--rtsp-only` filter — only show cameras that successfully returned an RTSP URL
- Authenticated scan (`--username` / `--password`) for cameras that require auth
- Per-device error isolation — a single camera failing does not break the entire scan

## Install

```bash
cd onvif-scanner

# Development mode (recommended)
pip install -e .

# Or run directly from source without installing
python -m onvif-scanner --help
```

Requires Python 3.10+.

## Usage

```bash
# Quick scan, pretty table output (default)
onvif-scan

# JSON to stdout
onvif-scan --output json

# Save to file
onvif-scan --output-file cameras.json

# Longer timeout (large / slow networks)
onvif-scan --timeout 10

# Authenticated scan (to get RTSP URLs from cameras that require auth)
onvif-scan --username admin --password secret

# Only show cameras that have an RTSP URL
onvif-scan --rtsp-only

# Debug logging (WS-Discovery + zeep)
onvif-scan --verbose
```

## JSON Output

```json
{
  "scan_time": "2026-07-19T10:00:00+07:00",
  "scan_duration_seconds": 3.2,
  "timeout_seconds": 5,
  "camera_count": 2,
  "cameras": [
    {
      "address": "192.168.1.100",
      "device_url": "http://192.168.1.100/onvif/device_service",
      "manufacturer": "Hikvision",
      "model": "DS-2CD2143G2",
      "firmware_version": "V5.6.5",
      "serial_number": "DS-2CD2143G2-...-VVV111100",
      "hardware_id": "880901",
      "rtsp_url": "rtsp://192.168.1.100:554/Streaming/Channels/101",
      "mac": null,
      "scopes": ["onvif://www.onvif.org/Profile/Streaming"],
      "authenticated": false,
      "error": null
    }
  ]
}
```

## Troubleshooting

| Symptom | Cause | Solution |
|---------|-------|----------|
| `No ONVIF devices discovered` | Multicast UDP blocked by firewall / different VLAN | Make sure UDP port 3702 (multicast 239.255.255.250:3702) is allowed. The scan must run on the same subnet as the cameras. |
| Camera discovered but `rtsp_url: null` | Camera requires auth for the Media service | Use `--username admin --password <pass>` |
| Camera not discovered despite being online | ONVIF disabled in the camera's web UI | Enable ONVIF (Profile S) in the camera settings |
| `ImportError: wsdiscovery` / `onvif-zeep` | Dependencies not installed | Re-run `pip install -e .` |
| Scan is slow (>5s) | Large network / many devices | Increase `--timeout 10`, or add `--rtsp-only` to skip device queries |
| zeep error: WSDL download | onvif-zeep requires internet access on first run (to download WSDL) | Ensure internet connectivity, or bundle the WSDL locally and pass `wsdl_dir` in `device.py` |

## Compatible Devices

Tested with: Hikvision, Dahua, generic ONVIF Profile S (Arducam, Reolink, Amcrest).

Standards: ONVIF Core Spec + Profile S (Streaming).

## Architecture

```
onvif_scanner/
├── cli.py          # Click CLI + rich output (table/json/file)
├── discovery.py    # WS-Discovery multicast via wsdiscovery lib
├── device.py       # ONVIF Device Service query via onvif-zeep
└── models.py       # dataclass DiscoveredDevice + Camera
```

Flow: `cli.py` → `discovery.discover_cameras()` (UDP multicast) → `device.query_device()` (HTTP SOAP per camera) → output (table/json/file).

## Limitations (future work)

- Multicast only (no unicast IP range probe for cross-VLAN discovery)
- Single username/password (no multi-credential support)
- No auto-registration to the parking DB (`cameras` table) — a `register` subcommand could be added
- No streaming preview (RTSP URL only, no frame preview)

## License

MIT
