diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8c29cd7..d8cbaa6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to the ONVIF Python project! We welc
2. **Create a new branch** for your feature, fix, or documentation update.
3. **Make your changes** with clear, descriptive commit messages.
4. **Test your changes** to ensure they work as expected and do not break existing functionality.
-5. **Push your branch** to your fork and open a Pull Request (PR) against the `main` branch of this repository.
+5. **Push your branch** to your fork and open a Pull Request (PR) against the `dev` branch of this repository.
6. **Participate in code review** by responding to feedback and making necessary updates.
## Code of Conduct
@@ -45,25 +45,41 @@ All contributors are expected to follow our [Code of Conduct](./CODE_OF_CONDUCT.
## Development Setup
-1. **Clone the repository:**
+1. **Clone the repository and switch to dev branch:**
```bash
+ # Option 1: Clone dev branch directly
+ git clone -b dev https://github.com/nirsimetri/onvif-python.git
+ cd onvif-python
+
+ # Option 2: Clone then switch to dev
git clone https://github.com/nirsimetri/onvif-python.git
cd onvif-python
+ git checkout dev
```
2. **Install locally:**
```bash
- pip install .
+ # Install the package in development mode
+ pip install -e .
+
+ # Install development dependencies (pytest, black, flake8)
+ pip install -e ".[dev]"
```
Or use `pyproject.toml` with your preferred tool (e.g., Poetry, pip).
-3. (Optional) **Run tests:**
+3. **Run tests:**
```bash
pytest
```
-4. (Optional) **Lint and format code:**
+ Make sure all tests pass before submitting your changes.
+
+4. **Lint and format code:**
```bash
+ # Check code style with flake8
flake8 .
+
+ # Format code with black
black .
```
+ Ensure your code follows PEP8 standards and is properly formatted.
5. **Try example scripts:**
See the [`examples/`](./examples/) folder for usage scenarios.
diff --git a/README.md b/README.md
index 0ac2210..447bf4c 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@

-

+

@@ -21,15 +21,21 @@
Behind the scenes, ONVIF communication relies on **[SOAP](https://en.wikipedia.org/wiki/SOAP) (Simple Object Access Protocol)** — an [XML](https://en.wikipedia.org/wiki/XML)-based messaging protocol with strict schema definitions ([WSDL](https://en.wikipedia.org/wiki/Web_Services_Description_Language)/[XSD](https://en.wikipedia.org/wiki/XML_Schema_(W3C))). SOAP ensures interoperability, but when used directly it can be verbose, complex, and error-prone.
-This library simplifies that process by wrapping SOAP communication into a clean, Pythonic API. You no longer need to handle low-level XML parsing, namespaces, or security tokens manually — the library takes care of it, letting you focus on building functionality.
+This library simplifies that process by wrapping SOAP communication into a clean, Pythonic API. You no longer need to handle low-level XML parsing, namespaces, or security tokens manually — the library takes care of it, letting you focus on building functionality.
-## Key Features
-- Full implementation of ONVIF core services and profiles
-- Support for device discovery, media streaming, PTZ control, event management, and more
-- Pythonic abstraction over SOAP requests and responses (no need to handcraft XML)
-- Extensible architecture for custom ONVIF extensions
-- Compatible with multiple ONVIF specification versions
-- Example scripts and tests included
+## Library Philosophy
+> [!NOTE]
+> This library will be continuously updated as ONVIF versions are updated. It uses a built-in WSDL that will always follow changes to the [ONVIF WSDL Specifications](https://github.com/onvif/specs). You can also use your own ONVIF WSDL file by adding the `wsdl_dir` argument; see [ONVIFClient Parameters](#onvifclient-parameters).
+
+- **WYSIWYG (What You See is What You Get)**: Every ONVIF operation in the library mirrors the official ONVIF specification exactly. Method names, parameter structures, and response formats follow ONVIF standards without abstraction layers or renamed interfaces. What you see in the ONVIF documentation is exactly what you get in Python.
+
+- **Device Variety Interoperability**: Built to handle the real-world diversity of ONVIF implementations across manufacturers. The library gracefully handles missing features, optional operations, and vendor-specific behaviors through comprehensive error handling and fallback mechanisms. Whether you're working with high-end enterprise cameras or budget IP cameras, the library adapts.
+
+- **Official Specifications Accuracy**: All service implementations are generated and validated against official `ONVIF WSDL Specifications`. The library includes comprehensive test suites that verify compliance with ONVIF standards, ensuring that method signatures, parameter types, and behavior match the official specifications precisely.
+
+- **Modern Python Approach**: Designed for excellent IDE support with full type hints, auto-completion, and immediate error detection. You'll get `TypeError` exceptions upfront when accessing ONVIF operations with wrong arguments, instead of cryptic `SOAP faults` later. Clean, Pythonic API that feels natural to Python developers while maintaining ONVIF compatibility.
+
+- **Minimal Dependencies**: Only depends on essential, well-maintained libraries (`zeep` for SOAP, `requests` for HTTP). No bloated framework dependencies or custom XML parsers. The library stays lightweight while providing full ONVIF functionality, making it easy to integrate into any project without dependency conflicts.
## Who Is It For?
- **Individual developers** exploring ONVIF or building hobby projects
@@ -308,11 +314,12 @@ This library includes a powerful command-line interface (CLI) for interacting wi
1. Direct CLI
```bash
-usage: onvif [-h] [--host HOST] [--port PORT] [--username USERNAME] [--password PASSWORD] [--discover] [--filter FILTER] [--search SEARCH] [--page PAGE] [--per-page PER_PAGE] [--timeout TIMEOUT] [--https]
- [--no-verify] [--no-patch] [--interactive] [--debug] [--wsdl WSDL] [--cache {all,db,mem,none}] [--health-check-interval HEALTH_CHECK_INTERVAL] [--version]
+usage: onvif [-h] [--host HOST] [--port PORT] [--username USERNAME] [--password PASSWORD] [--discover] [--filter FILTER] [--search SEARCH] [--page PAGE]
+ [--per-page PER_PAGE] [--timeout TIMEOUT] [--https] [--no-verify] [--no-patch] [--interactive] [--debug] [--wsdl WSDL]
+ [--cache {all,db,mem,none}] [--health-check-interval HEALTH_CHECK_INTERVAL] [--output OUTPUT] [--version]
[service] [method] [params ...]
-ONVIF Terminal Client — v0.2.4
+ONVIF Terminal Client — v0.2.5
https://github.com/nirsimetri/onvif-python
positional arguments:
@@ -346,6 +353,9 @@ options:
Caching mode for ONVIFClient (default: all). 'all': memory+disk, 'db': disk-only, 'mem': memory-only, 'none': disabled.
--health-check-interval HEALTH_CHECK_INTERVAL, -hci HEALTH_CHECK_INTERVAL
Health check interval in seconds for interactive mode (default: 10)
+ --output OUTPUT, -o OUTPUT
+ Save command output to file. Supports .json, .xml extensions for format detection, or plain text. XML format automatically enables
+ debug mode for SOAP capture.
--version, -v Show ONVIF CLI version and exit
Examples:
@@ -366,7 +376,12 @@ Examples:
# Direct command execution
onvif devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
- onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity={"PanTilt": {"x": -0.1, "y": 0}} --host 192.168.1.17 --port 8000 --username admin --password admin123
+ onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity={'PanTilt': {'x': -0.1, 'y': 0}} -H 192.168.1.17 -P 8000 -u admin -p admin123
+
+ # Save output to file
+ onvif devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
+ onvif media GetProfiles --host 192.168.1.17 --port 8000 --username admin --password admin123 --output profiles.xml
+ onvif ptz GetConfigurations --host 192.168.1.17 --port 8000 --username admin --password admin123 --output ptz_config.txt --debug
# Interactive mode
onvif --host 192.168.1.17 --port 8000 --username admin --password admin123 --interactive
@@ -385,7 +400,7 @@ Examples:
2. Interactive Shell
```bash
-ONVIF Interactive Shell — v0.2.4
+ONVIF Interactive Shell — v0.2.5
https://github.com/nirsimetri/onvif-python
Basic Commands:
@@ -593,7 +608,11 @@ onvif
[parameters...] -H -P -u -p
onvif devicemgmt GetCapabilities Category=All -H 192.168.1.17 -P 8000 -u admin -p admin123
# Move a PTZ camera
-onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity='{"PanTilt": {"x": 0.1}}' -H 192.168.1.17 -P 8000 -u admin -p admin123
+onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity='{"PanTilt": {"x": 0.1, "y": 0}}' -H 192.168.1.17 -P 8000 -u admin -p admin123
+
+# Save output to file
+onvif devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
+onvif media GetProfiles -H 192.168.1.17 -P 8000 -u admin -p admin123 -o profiles.xml
```
**4. ONVIF Product Search**
@@ -1109,13 +1128,11 @@ Some ONVIF services have multiple bindings in the same WSDL. These typically inc
- [ ] Add more usage examples for advanced features.
- [ ] Add benchmarking and performance metrics.
- [ ] Add community-contributed device configuration templates.
-- [ ] Implement missing or partial ONVIF services.
-- [ ] Add function to expose ONVIF devices (for debugging purposes by the community).
## Related Projects
- [onvif-products-directory](https://github.com/nirsimetri/onvif-products-directory):
- This project is a comprehensive ONVIF data aggregation and management suite, designed to help developers explore, analyze, and process ONVIF-compliant product information from hundreds of manufacturers worldwide. It provides a unified structure for device, client, and company data, making it easier to perform research, build integrations, and generate statistics for ONVIF ecosystem analysis.
+ This project is a comprehensive ONVIF data aggregation and management suite, designed to help developers explore, analyze, and process ONVIF-compliant product information from hundreds of manufacturers worldwide.
- (soon) [onvif-rest-server](https://github.com/nirsimetri/onvif-rest-server):
A RESTful API server for ONVIF devices, enabling easy integration of ONVIF device management, media streaming, and other capabilities into web applications and services.
diff --git a/README_ID.md b/README_ID.md
index 4c79c3e..04b1847 100644
--- a/README_ID.md
+++ b/README_ID.md
@@ -3,7 +3,7 @@

-

+

@@ -23,13 +23,19 @@ Di balik layar, komunikasi ONVIF bergantung pada **[SOAP](https://en.wikipedia.o
Pustaka ini menyederhanakan proses tersebut dengan membungkus komunikasi SOAP ke dalam API Python yang bersih. Anda tidak perlu lagi menangani parsing XML tingkat rendah, namespace, atau token keamanan secara manual — pustaka ini menangani semuanya, memungkinkan Anda untuk fokus pada pembangunan fungsionalitas.
-## Fitur Utama
-- Implementasi penuh layanan inti dan profil ONVIF
-- Dukungan untuk penemuan perangkat, streaming media, kontrol PTZ, manajemen event, dan lainnya
-- Abstraksi Pythonic atas permintaan dan respons SOAP (tidak perlu membuat XML secara manual)
-- Arsitektur yang dapat diperluas untuk ekstensi ONVIF khusus
-- Kompatibel dengan beberapa versi spesifikasi ONVIF
-- Skrip contoh dan pengujian disertakan
+## Filosofi Pustaka
+> [!NOTE]
+> Pustaka ini akan terus diperbarui seiring dengan pembaruan versi ONVIF. Pustaka menggunakan WSDL bawaan yang akan selalu mengikuti perubahan pada [Spesifikasi WSDL ONVIF](https://github.com/onvif/specs). Anda juga dapat menggunakan file WSDL ONVIF Anda sendiri dengan menambahkan argumen `wsdl_dir`; lihat [Parameter ONVIFClient](#parameter-onvifclient).
+
+- **WYSIWYG (What You See is What You Get)**: Setiap operasi ONVIF dalam pustaka ini mencerminkan spesifikasi ONVIF resmi secara tepat. Nama metode, struktur parameter, dan format respons mengikuti standar ONVIF tanpa lapisan abstraksi atau antarmuka yang diubah namanya. Apa yang Anda lihat dalam dokumentasi ONVIF adalah persis apa yang Anda dapatkan dalam Python.
+
+- **Interoperabilitas Ragam Perangkat**: Dibangun untuk menangani keragaman dunia nyata dari implementasi ONVIF lintas produsen. Pustaka ini dengan anggun menangani fitur yang hilang, operasi opsional, dan perilaku spesifik vendor melalui penanganan kesalahan yang komprehensif dan mekanisme fallback. Baik Anda bekerja dengan kamera enterprise kelas atas atau kamera IP murah, pustaka ini akan beradaptasi.
+
+- **Akurasi Spesifikasi Resmi**: Semua implementasi layanan dihasilkan dan divalidasi terhadap `Spesifikasi WSDL ONVIF` resmi. Pustaka ini mencakup test suite komprehensif yang memverifikasi kepatuhan terhadap standar ONVIF, memastikan bahwa tanda tangan metode, tipe parameter, dan perilaku cocok dengan spesifikasi resmi secara tepat.
+
+- **Pendekatan Python Modern**: Dirancang untuk dukungan IDE yang sangat baik dengan type hints lengkap, auto-completion, dan deteksi kesalahan langsung. Anda akan mendapatkan pengecualian `TypeError` di awal saat mengakses operasi ONVIF dengan argumen yang salah, alih-alih `SOAP faults` yang tidak jelas kemudian. API Python yang bersih dan natural bagi pengembang Python sambil mempertahankan kompatibilitas ONVIF.
+
+- **Dependensi Minimal**: Hanya bergantung pada pustaka penting yang terpelihara dengan baik (`zeep` untuk SOAP, `requests` untuk HTTP). Tidak ada dependensi framework yang membengkak atau parser XML kustom. Pustaka tetap ringan sambil menyediakan fungsionalitas ONVIF penuh, membuatnya mudah diintegrasikan ke dalam proyek apa pun tanpa konflik dependensi.
## Untuk Siapa Pustaka Ini?
- **Pengembang individu** yang menjelajahi ONVIF atau membangun proyek hobi
@@ -308,11 +314,12 @@ Pustaka ini menyertakan antarmuka baris perintah (CLI) yang kuat untuk berintera
1. CLI Langsung
```bash
-usage: onvif [-h] [--host HOST] [--port PORT] [--username USERNAME] [--password PASSWORD] [--discover] [--filter FILTER] [--search SEARCH] [--page PAGE] [--per-page PER_PAGE] [--timeout TIMEOUT] [--https]
- [--no-verify] [--no-patch] [--interactive] [--debug] [--wsdl WSDL] [--cache {all,db,mem,none}] [--health-check-interval HEALTH_CHECK_INTERVAL] [--version]
+usage: onvif [-h] [--host HOST] [--port PORT] [--username USERNAME] [--password PASSWORD] [--discover] [--filter FILTER] [--search SEARCH] [--page PAGE]
+ [--per-page PER_PAGE] [--timeout TIMEOUT] [--https] [--no-verify] [--no-patch] [--interactive] [--debug] [--wsdl WSDL]
+ [--cache {all,db,mem,none}] [--health-check-interval HEALTH_CHECK_INTERVAL] [--output OUTPUT] [--version]
[service] [method] [params ...]
-ONVIF Terminal Client — v0.2.4
+ONVIF Terminal Client — v0.2.5
https://github.com/nirsimetri/onvif-python
positional arguments:
@@ -346,6 +353,9 @@ options:
Caching mode for ONVIFClient (default: all). 'all': memory+disk, 'db': disk-only, 'mem': memory-only, 'none': disabled.
--health-check-interval HEALTH_CHECK_INTERVAL, -hci HEALTH_CHECK_INTERVAL
Health check interval in seconds for interactive mode (default: 10)
+ --output OUTPUT, -o OUTPUT
+ Save command output to file. Supports .json, .xml extensions for format detection, or plain text. XML format automatically enables
+ debug mode for SOAP capture.
--version, -v Show ONVIF CLI version and exit
Examples:
@@ -366,7 +376,12 @@ Examples:
# Direct command execution
onvif devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
- onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity={"PanTilt": {"x": -0.1, "y": 0}} --host 192.168.1.17 --port 8000 --username admin --password admin123
+ onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity={'PanTilt': {'x': -0.1, 'y': 0}} -H 192.168.1.17 -P 8000 -u admin -p admin123
+
+ # Save output to file
+ onvif devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
+ onvif media GetProfiles --host 192.168.1.17 --port 8000 --username admin --password admin123 --output profiles.xml
+ onvif ptz GetConfigurations --host 192.168.1.17 --port 8000 --username admin --password admin123 --output ptz_config.txt --debug
# Interactive mode
onvif --host 192.168.1.17 --port 8000 --username admin --password admin123 --interactive
@@ -386,7 +401,7 @@ Examples:
```bash
-ONVIF Interactive Shell — v0.2.4
+ONVIF Interactive Shell — v0.2.5
https://github.com/nirsimetri/onvif-python
Basic Commands:
@@ -594,7 +609,11 @@ onvif
[parameters...] -H -P -u -p
onvif devicemgmt GetCapabilities Category=All -H 192.168.1.17 -P 8000 -u admin -p admin123
# Gerakkan kamera PTZ
-onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity='{"PanTilt": {"x": 0.1}}' -H 192.168.1.17 -P 8000 -u admin -p admin123
+onvif ptz ContinuousMove ProfileToken=Profile_1 Velocity='{"PanTilt": {"x": 0.1, "y": 0}}' -H 192.168.1.17 -P 8000 -u admin -p admin123
+
+# Simpan output ke file
+onvif devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
+onvif media GetProfiles -H 192.168.1.17 -P 8000 -u admin -p admin123 -o profiles.xml
```
**4. Pencarian Produk ONVIF**
@@ -1109,13 +1128,11 @@ Beberapa layanan ONVIF memiliki banyak binding dalam WSDL yang sama. Biasanya me
- [ ] Menambahkan lebih banyak contoh penggunaan untuk fitur lanjutan.
- [ ] Menambahkan benchmarking dan metrik performa.
- [ ] Menambahkan template konfigurasi perangkat yang dikontribusikan oleh komunitas.
-- [ ] Mengimplementasikan layanan ONVIF yang hilang atau masih parsial.
-- [ ] Menambahkan fungsi untuk mengekspos perangkat ONVIF (untuk tujuan debugging oleh komunitas).
## Proyek Terkait
- [onvif-products-directory](https://github.com/nirsimetri/onvif-products-directory):
- Proyek ini adalah suite agregasi dan manajemen data ONVIF yang komprehensif, dirancang untuk membantu pengembang menelusuri, menganalisis, dan memproses informasi produk yang sesuai ONVIF dari ratusan produsen di seluruh dunia. Menyediakan struktur terpadu untuk data perangkat, klien, dan perusahaan, sehingga mempermudah riset, membangun integrasi, dan menghasilkan statistik untuk analisis ekosistem ONVIF.
+ Proyek ini adalah suite agregasi dan manajemen data ONVIF yang komprehensif, dirancang untuk membantu pengembang menelusuri, menganalisis, dan memproses informasi produk yang sesuai ONVIF dari ratusan produsen di seluruh dunia.
- (segera) [onvif-rest-server](https://github.com/nirsimetri/onvif-rest-server):
Server API RESTful untuk perangkat ONVIF, memungkinkan integrasi mudah manajemen perangkat ONVIF, streaming media, dan kemampuan lainnya ke aplikasi dan layanan web.
diff --git a/examples/add_media_profile.py b/examples/add_media_profile.py
index 8208fae..22efb1e 100644
--- a/examples/add_media_profile.py
+++ b/examples/add_media_profile.py
@@ -84,7 +84,7 @@
# Verify the profile was created
print("\nVerifying profile creation...")
profile = media.GetProfile(ProfileToken=profile_token)
- print(f"\nProfile Details:")
+ print("\nProfile Details:")
print(f" Name: {profile.Name}")
print(f" Token: {profile.token}")
if (
diff --git a/examples/error_handling.py b/examples/error_handling.py
index d386303..c8d9473 100644
--- a/examples/error_handling.py
+++ b/examples/error_handling.py
@@ -103,7 +103,7 @@ def example_3_manual_handling():
# Try GetSystemUris (not always supported), fallback to alternative
try:
system_uris = device.GetSystemUris()
- print(f"✓ System URIs:")
+ print("✓ System URIs:")
# System Log URIs (can be multiple)
if hasattr(system_uris, "SystemLogUris") and system_uris.SystemLogUris:
@@ -134,7 +134,7 @@ def example_3_manual_handling():
# Fallback: Get basic device information
device_info = device.GetDeviceInformation()
- print(f"✓ Device Information (alternative):")
+ print("✓ Device Information (alternative):")
print(f" Manufacturer: {getattr(device_info, 'Manufacturer', 'N/A')}")
print(f" Model: {getattr(device_info, 'Model', 'N/A')}")
print(
@@ -215,7 +215,7 @@ def example_5_critical_operations():
lambda: device.GetDeviceInformation(),
ignore_unsupported=False, # Raise exception if not supported
)
- print(f"✓ Device Info (critical):")
+ print("✓ Device Info (critical):")
print(f" Manufacturer: {getattr(device_info, 'Manufacturer', 'N/A')}")
print(f" Model: {getattr(device_info, 'Model', 'N/A')}")
print(f" FirmwareVersion: {getattr(device_info, 'FirmwareVersion', 'N/A')}")
diff --git a/examples/logger.py b/examples/logger.py
index 3d41b70..cca8092 100644
--- a/examples/logger.py
+++ b/examples/logger.py
@@ -84,7 +84,7 @@ def demonstrate_discovery_logging():
print("\n1. Starting device discovery...")
devices = discovery.discover(prefer_https=True, search=None)
- print(f"\n2. Discovery Results:")
+ print("\n2. Discovery Results:")
if devices:
for i, device in enumerate(devices, 1):
print(f" Device {i}:")
diff --git a/onvif/__init__.py b/onvif/__init__.py
index 9cec062..1e7f46f 100644
--- a/onvif/__init__.py
+++ b/onvif/__init__.py
@@ -1,7 +1,7 @@
# onvif/__init__.py
from .client import ONVIFClient
-from .operator import ONVIFOperator, CacheMode
+from .operator import CacheMode
from .utils import (
ONVIFWSDL,
ONVIFOperationException,
@@ -14,7 +14,6 @@
__all__ = [
"ONVIFClient",
- "ONVIFOperator",
"CacheMode",
"ONVIFWSDL",
"ONVIFOperationException",
diff --git a/onvif/cli/interactive.py b/onvif/cli/interactive.py
index da4503a..7bc5201 100644
--- a/onvif/cli/interactive.py
+++ b/onvif/cli/interactive.py
@@ -194,7 +194,7 @@ def __init__(self, client: ONVIFClient, args):
" / __ \\/ | / / | / / _/ ____/",
" / / / / |/ /| | / // // /_ ",
"/ /_/ / /| / | |/ // // __/ ",
- "\\____/_/ |_/ |___/___/_/ v0.2.4",
+ "\\____/_/ |_/ |___/___/_/ v0.2.5",
" ",
]
@@ -1400,7 +1400,7 @@ def do_help(self, line):
super().do_help(line)
else:
help_text = f"""
-{colorize('ONVIF Interactive Shell — v0.2.4', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
+{colorize('ONVIF Interactive Shell — v0.2.5', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
{colorize('Basic Commands:', 'yellow')}
capabilities, caps - Show device capabilities
diff --git a/onvif/cli/main.py b/onvif/cli/main.py
index 455cdb2..2003a9e 100644
--- a/onvif/cli/main.py
+++ b/onvif/cli/main.py
@@ -7,6 +7,7 @@
import sqlite3
import os
import shutil
+import json
from datetime import datetime
from typing import Any, Optional, Tuple
@@ -21,7 +22,7 @@ def create_parser():
"""Create argument parser for ONVIF CLI"""
parser = argparse.ArgumentParser(
prog="onvif",
- description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.4\nhttps://github.com/nirsimetri/onvif-python",
+ description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.5\nhttps://github.com/nirsimetri/onvif-python",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Examples:
@@ -42,7 +43,12 @@ def create_parser():
# Direct command execution
{colorize('onvif', 'yellow')} devicemgmt GetCapabilities Category=All --host 192.168.1.17 --port 8000 --username admin --password admin123
- {colorize('onvif', 'yellow')} ptz ContinuousMove ProfileToken=Profile_1 Velocity={{"PanTilt": {{"x": -0.1, "y": 0}}}} --host 192.168.1.17 --port 8000 --username admin --password admin123
+ {colorize('onvif', 'yellow')} ptz ContinuousMove ProfileToken=Profile_1 Velocity={{'PanTilt': {{'x': -0.1, 'y': 0}}}} -H 192.168.1.17 -P 8000 -u admin -p admin123
+
+ # Save output to file
+ {colorize('onvif', 'yellow')} devicemgmt GetDeviceInformation --host 192.168.1.17 --port 8000 --username admin --password admin123 --output device_info.json
+ {colorize('onvif', 'yellow')} media GetProfiles --host 192.168.1.17 --port 8000 --username admin --password admin123 --output profiles.xml
+ {colorize('onvif', 'yellow')} ptz GetConfigurations --host 192.168.1.17 --port 8000 --username admin --password admin123 --output ptz_config.txt --debug
# Interactive mode
{colorize('onvif', 'yellow')} --host 192.168.1.17 --port 8000 --username admin --password admin123 --interactive
@@ -139,6 +145,11 @@ def create_parser():
default=10,
help="Health check interval in seconds for interactive mode (default: 10)",
)
+ parser.add_argument(
+ "--output",
+ "-o",
+ help="Save command output to file. Supports .json, .xml extensions for format detection, or plain text. XML format automatically enables debug mode for SOAP capture.",
+ )
# Service and method (for direct command execution)
parser.add_argument(
@@ -177,7 +188,7 @@ def main():
# Show ONVIF CLI version
if args.version:
- print(colorize("0.2.4", "yellow"))
+ print(colorize("0.2.5", "yellow"))
sys.exit(0)
# Handle product search
@@ -196,6 +207,12 @@ def main():
f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} mode or {colorize('service/method', 'white')} must be specified"
)
+ # Validate output argument
+ if args.output and args.interactive:
+ parser.error(
+ f"{colorize('--output', 'white')} cannot be used with {colorize('--interactive', 'white')} mode"
+ )
+
# Handle discovery mode
if args.discover:
if args.host:
@@ -260,6 +277,11 @@ def main():
try:
# Create ONVIF client
+ # Auto-enable debug mode if output format is XML
+ auto_debug = args.debug or (
+ args.output and args.output.lower().endswith(".xml")
+ )
+
client = ONVIFClient(
host=args.host,
port=args.port,
@@ -270,7 +292,7 @@ def main():
use_https=args.https,
verify_ssl=not args.no_verify,
apply_patch=not args.no_patch,
- capture_xml=args.debug,
+ capture_xml=auto_debug,
wsdl_dir=args.wsdl,
)
@@ -298,7 +320,17 @@ def main():
# Execute direct command
params_str = " ".join(args.params) if args.params else None
result = execute_command(client, args.service, args.method, params_str)
- print(str(result))
+
+ # Save output to file if specified
+ if args.output:
+ # Auto-enable debug mode for XML output
+ effective_debug = args.debug or args.output.lower().endswith(".xml")
+ save_output_to_file(result, args.output, effective_debug, client)
+ print(
+ f"{colorize('Output saved to:', 'green')} {colorize(args.output, 'white')}"
+ )
+ else:
+ print(str(result))
except KeyboardInterrupt:
print("\nOperation cancelled by user")
@@ -337,6 +369,182 @@ def execute_command(
return method(**params)
+def save_output_to_file(
+ result: Any, output_path: str, debug_mode: bool, client: ONVIFClient
+) -> None:
+ """Save command output to file in appropriate format based on file extension.
+
+ Args:
+ result: The ONVIF command result
+ output_path: Path to output file
+ debug_mode: Whether debug mode is enabled (for XML capture)
+ client: ONVIFClient instance (for accessing XML plugin)
+ """
+ try:
+ # Determine output format based on file extension
+ _, ext = os.path.splitext(output_path.lower())
+
+ if ext == ".json":
+ # Prepare output data
+ output_data = {}
+
+ # JSON format
+ output_data["result"] = _serialize_for_json(result)
+ output_data["timestamp"] = datetime.now().isoformat()
+ output_data["raw_result"] = str(result) # Add raw string as fallback
+
+ # Add XML data if debug mode is enabled and XML plugin is available
+ if debug_mode and client.xml_plugin:
+ output_data["debug"] = {
+ "last_request_xml": client.xml_plugin.last_sent_xml,
+ "last_response_xml": client.xml_plugin.last_received_xml,
+ "last_operation": client.xml_plugin.last_operation,
+ }
+
+ with open(output_path, "w", encoding="utf-8") as f:
+ json.dump(output_data, f, indent=2, ensure_ascii=False)
+
+ elif ext == ".xml":
+ # XML format - prioritize raw SOAP XML over parsed result
+ if client.xml_plugin and client.xml_plugin.last_received_xml:
+ # Save the raw SOAP response XML with minimal wrapper
+ content = f"""
+
+
+
+
+{client.xml_plugin.last_received_xml}
+"""
+ else:
+ # Fallback: Simple XML wrapper for the parsed result
+ content = f"""
+
+
+
+
+
+
+"""
+
+ with open(output_path, "w", encoding="utf-8") as f:
+ f.write(content)
+
+ else:
+ # Plain text format (default)
+ content = "ONVIF Command Output\n"
+ content += f"Timestamp: {datetime.now().isoformat()}\n"
+ content += f"{'='*50}\n\n"
+ content += str(result)
+
+ # Add debug information if available
+ if debug_mode and client.xml_plugin:
+ content += f"\n\n{'='*50}\n"
+ content += "DEBUG INFORMATION\n"
+ content += f"{'='*50}\n"
+ if client.xml_plugin.last_operation:
+ content += f"Operation: {client.xml_plugin.last_operation}\n\n"
+ if client.xml_plugin.last_sent_xml:
+ content += "SOAP Request:\n"
+ content += client.xml_plugin.last_sent_xml + "\n\n"
+ if client.xml_plugin.last_received_xml:
+ content += "SOAP Response:\n"
+ content += client.xml_plugin.last_received_xml + "\n"
+
+ with open(output_path, "w", encoding="utf-8") as f:
+ f.write(content)
+
+ except Exception as e:
+ print(f"{colorize('Error saving output:', 'red')} {e}", file=sys.stderr)
+ # Still print the result to console if file save fails
+ print(str(result))
+
+
+def _serialize_for_json(obj: Any) -> Any:
+ """Recursively serialize ONVIF objects for JSON output.
+
+ Args:
+ obj: Object to serialize
+
+ Returns:
+ JSON-serializable representation of the object
+ """
+ if obj is None:
+ return None
+ elif isinstance(obj, (str, int, float, bool)):
+ return obj
+ elif isinstance(obj, datetime):
+ return obj.isoformat()
+ elif isinstance(obj, (list, tuple)):
+ return [_serialize_for_json(item) for item in obj]
+ elif isinstance(obj, dict):
+ return {key: _serialize_for_json(value) for key, value in obj.items()}
+
+ # Check if this is a Zeep object (has _xsd_type attribute)
+ elif hasattr(obj, "_xsd_type"):
+ result = {}
+ # Try to get all elements from XSD type
+ if hasattr(obj._xsd_type, "elements"):
+ for elem_name, elem_obj in obj._xsd_type.elements:
+ try:
+ value = getattr(obj, elem_name, None)
+ if value is not None:
+ result[elem_name] = _serialize_for_json(value)
+ except (AttributeError, TypeError):
+ # Skip elements that can't be accessed or have type issues
+ pass
+
+ # Also try regular attributes
+ for attr_name in dir(obj):
+ if not attr_name.startswith("_") and not callable(
+ getattr(obj, attr_name, None)
+ ):
+ try:
+ attr_value = getattr(obj, attr_name)
+ if attr_value is not None and attr_name not in result:
+ result[attr_name] = _serialize_for_json(attr_value)
+ except (AttributeError, TypeError):
+ # Skip attributes that can't be accessed or have type issues
+ pass
+
+ return result
+
+ elif hasattr(obj, "__dict__"):
+ # Handle regular objects with attributes
+ result = {}
+ for key, value in obj.__dict__.items():
+ if not key.startswith("_"): # Skip private attributes
+ result[key] = _serialize_for_json(value)
+
+ # If result is empty, try to get attributes using dir()
+ if not result:
+ for attr_name in dir(obj):
+ if not attr_name.startswith("_") and not callable(
+ getattr(obj, attr_name, None)
+ ):
+ try:
+ attr_value = getattr(obj, attr_name)
+ if attr_value is not None:
+ result[attr_name] = _serialize_for_json(attr_value)
+ except (AttributeError, TypeError):
+ # Skip attributes that can't be accessed or have type issues
+ pass
+
+ return result
+ elif hasattr(obj, "_value_1"):
+ # Handle zeep objects with special structure
+ return _serialize_for_json(obj._value_1)
+ else:
+ # Try to convert to dict using vars() if available
+ try:
+ obj_dict = vars(obj)
+ return _serialize_for_json(obj_dict)
+ except TypeError:
+ # Fallback to string representation
+ return str(obj)
+
+
def discover_devices(
timeout: int = 4, prefer_https: bool = False, filter_term: Optional[str] = None
) -> list:
diff --git a/pyproject.toml b/pyproject.toml
index cd2e6e9..b17ccca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "onvif-python"
-version = "0.2.4"
+version = "0.2.5"
description = "A modern Python library for ONVIF-compliant devices"
readme = "README.md"
requires-python = ">=3.9"