onvif-python 0.2.11__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {onvif_python-0.2.11 → onvif_python-0.3.1}/LICENSE.md +1 -1
- onvif_python-0.3.1/PKG-INFO +103 -0
- onvif_python-0.3.1/README.md +56 -0
- onvif_python-0.3.1/onvif/__init__.py +25 -0
- onvif_python-0.3.1/onvif/cli/__init__.py +5 -0
- onvif_python-0.3.1/onvif/cli/__main__.py +7 -0
- onvif_python-0.3.1/onvif/cli/helpers/__init__.py +21 -0
- onvif_python-0.3.1/onvif/cli/helpers/discovery.py +124 -0
- onvif_python-0.3.1/onvif/cli/helpers/execution.py +57 -0
- onvif_python-0.3.1/onvif/cli/helpers/files.py +189 -0
- onvif_python-0.3.1/onvif/cli/helpers/messages.py +221 -0
- onvif_python-0.3.1/onvif/cli/helpers/products.py +292 -0
- onvif_python-0.2.11/onvif/cli/utils.py → onvif_python-0.3.1/onvif/cli/helpers/wsdl_parser.py +132 -707
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/cli/interactive.py +97 -243
- onvif_python-0.3.1/onvif/cli/main.py +336 -0
- onvif_python-0.3.1/onvif/cli/utils.py +508 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/client.py +292 -207
- onvif_python-0.3.1/onvif/meta.py +6 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/operator.py +107 -49
- onvif_python-0.3.1/onvif/services/__init__.py +79 -0
- onvif_python-0.3.1/onvif/services/accesscontrol.py +291 -0
- onvif_python-0.3.1/onvif/services/accessrules.py +127 -0
- onvif_python-0.3.1/onvif/services/actionengine.py +108 -0
- onvif_python-0.3.1/onvif/services/analytics/__init__.py +0 -0
- onvif_python-0.3.1/onvif/services/analytics/analytics.py +123 -0
- onvif_python-0.3.1/onvif/services/analytics/ruleengine.py +77 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/services/analyticsdevice.py +58 -23
- onvif_python-0.3.1/onvif/services/appmgmt.py +73 -0
- onvif_python-0.3.1/onvif/services/authenticationbehavior.py +218 -0
- onvif_python-0.3.1/onvif/services/credential.py +320 -0
- onvif_python-0.3.1/onvif/services/deviceio.py +326 -0
- onvif_python-0.3.1/onvif/services/devicemgmt.py +966 -0
- onvif_python-0.3.1/onvif/services/display.py +131 -0
- onvif_python-0.3.1/onvif/services/doorcontrol.py +266 -0
- onvif_python-0.3.1/onvif/services/events/__init__.py +0 -0
- onvif_python-0.3.1/onvif/services/events/events.py +86 -0
- onvif_python-0.3.1/onvif/services/events/notification.py +71 -0
- onvif_python-0.3.1/onvif/services/events/pausable_subscription.py +72 -0
- onvif_python-0.3.1/onvif/services/events/pullpoint.py +79 -0
- onvif_python-0.3.1/onvif/services/events/subscription.py +52 -0
- onvif_python-0.3.1/onvif/services/imaging.py +142 -0
- onvif_python-0.3.1/onvif/services/media.py +885 -0
- onvif_python-0.3.1/onvif/services/media2.py +665 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/services/provisioning.py +22 -8
- onvif_python-0.3.1/onvif/services/ptz.py +379 -0
- onvif_python-0.3.1/onvif/services/receiver.py +93 -0
- onvif_python-0.3.1/onvif/services/recording.py +288 -0
- onvif_python-0.3.1/onvif/services/replay.py +59 -0
- onvif_python-0.3.1/onvif/services/schedule.py +181 -0
- onvif_python-0.3.1/onvif/services/search.py +402 -0
- onvif_python-0.3.1/onvif/services/security/__init__.py +0 -0
- onvif_python-0.3.1/onvif/services/security/advancedsecurity.py +34 -0
- onvif_python-0.3.1/onvif/services/security/authorizationserver.py +52 -0
- onvif_python-0.3.1/onvif/services/security/dot1x.py +160 -0
- onvif_python-0.3.1/onvif/services/security/jwt.py +36 -0
- onvif_python-0.3.1/onvif/services/security/keystore.py +611 -0
- onvif_python-0.3.1/onvif/services/security/mediasigning.py +77 -0
- onvif_python-0.3.1/onvif/services/security/tlsserver.py +261 -0
- onvif_python-0.3.1/onvif/services/thermal.py +87 -0
- onvif_python-0.3.1/onvif/services/uplink.py +55 -0
- onvif_python-0.3.1/onvif/utils/__init__.py +21 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/discovery.py +30 -44
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/error_handlers.py +30 -21
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/exceptions.py +34 -28
- onvif_python-0.2.11/onvif/utils/parser.py → onvif_python-0.3.1/onvif/utils/plugins.py +41 -8
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/service.py +11 -14
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/wsdl.py +52 -36
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/xml_capture.py +15 -18
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/utils/zeep.py +13 -19
- onvif_python-0.3.1/onvif_python.egg-info/PKG-INFO +103 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif_python.egg-info/SOURCES.txt +12 -1
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif_python.egg-info/requires.txt +4 -1
- {onvif_python-0.2.11 → onvif_python-0.3.1}/pyproject.toml +35 -2
- {onvif_python-0.2.11 → onvif_python-0.3.1}/setup.py +2 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/tests/test_client.py +24 -24
- {onvif_python-0.2.11 → onvif_python-0.3.1}/tests/test_core.py +63 -34
- {onvif_python-0.2.11 → onvif_python-0.3.1}/tests/test_error_handlers.py +22 -22
- {onvif_python-0.2.11 → onvif_python-0.3.1}/tests/test_exceptions.py +21 -21
- onvif_python-0.2.11/PKG-INFO +0 -1219
- onvif_python-0.2.11/README.md +0 -1173
- onvif_python-0.2.11/onvif/__init__.py +0 -33
- onvif_python-0.2.11/onvif/cli/__init__.py +0 -5
- onvif_python-0.2.11/onvif/cli/__main__.py +0 -7
- onvif_python-0.2.11/onvif/cli/main.py +0 -977
- onvif_python-0.2.11/onvif/services/__init__.py +0 -79
- onvif_python-0.2.11/onvif/services/accesscontrol.py +0 -121
- onvif_python-0.2.11/onvif/services/accessrules.py +0 -52
- onvif_python-0.2.11/onvif/services/actionengine.py +0 -51
- onvif_python-0.2.11/onvif/services/analytics/analytics.py +0 -65
- onvif_python-0.2.11/onvif/services/analytics/ruleengine.py +0 -49
- onvif_python-0.2.11/onvif/services/appmgmt.py +0 -45
- onvif_python-0.2.11/onvif/services/authenticationbehavior.py +0 -88
- onvif_python-0.2.11/onvif/services/credential.py +0 -164
- onvif_python-0.2.11/onvif/services/deviceio.py +0 -170
- onvif_python-0.2.11/onvif/services/devicemgmt.py +0 -425
- onvif_python-0.2.11/onvif/services/display.py +0 -67
- onvif_python-0.2.11/onvif/services/doorcontrol.py +0 -98
- onvif_python-0.2.11/onvif/services/events/events.py +0 -46
- onvif_python-0.2.11/onvif/services/events/notification.py +0 -38
- onvif_python-0.2.11/onvif/services/events/pausable_subscription.py +0 -31
- onvif_python-0.2.11/onvif/services/events/pullpoint.py +0 -33
- onvif_python-0.2.11/onvif/services/events/subscription.py +0 -25
- onvif_python-0.2.11/onvif/services/imaging.py +0 -69
- onvif_python-0.2.11/onvif/services/media.py +0 -429
- onvif_python-0.2.11/onvif/services/media2.py +0 -340
- onvif_python-0.2.11/onvif/services/ptz.py +0 -192
- onvif_python-0.2.11/onvif/services/receiver.py +0 -51
- onvif_python-0.2.11/onvif/services/recording.py +0 -171
- onvif_python-0.2.11/onvif/services/replay.py +0 -35
- onvif_python-0.2.11/onvif/services/schedule.py +0 -87
- onvif_python-0.2.11/onvif/services/search.py +0 -218
- onvif_python-0.2.11/onvif/services/security/advancedsecurity.py +0 -24
- onvif_python-0.2.11/onvif/services/security/authorizationserver.py +0 -36
- onvif_python-0.2.11/onvif/services/security/dot1x.py +0 -47
- onvif_python-0.2.11/onvif/services/security/jwt.py +0 -26
- onvif_python-0.2.11/onvif/services/security/keystore.py +0 -213
- onvif_python-0.2.11/onvif/services/security/mediasigning.py +0 -35
- onvif_python-0.2.11/onvif/services/security/tlsserver.py +0 -87
- onvif_python-0.2.11/onvif/services/thermal.py +0 -59
- onvif_python-0.2.11/onvif/services/uplink.py +0 -33
- onvif_python-0.2.11/onvif/utils/__init__.py +0 -21
- onvif_python-0.2.11/onvif_python.egg-info/PKG-INFO +0 -1219
- {onvif_python-0.2.11 → onvif_python-0.3.1}/MANIFEST.in +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/db/companies.db +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/db/products.db +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/LICENSE.md +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/b-2.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/bf-2.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/bw-2.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/include +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/mpqf.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/r-2.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/rw-2.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/soap-envelope +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/t-1.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/ws-addr.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/xml.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/schema/common/xmlmime +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/accesscontrol/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/accessrules/wsdl/accessrules.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/accessrules/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/actionengine.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/advancedsecurity/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/analyticsdevice.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/appmgmt/wsdl/appmgmt.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/appmgmt/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/authenticationbehavior/wsdl/authenticationbehavior.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/authenticationbehavior/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/credential/wsdl/credential.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/credential/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/device/wsdl/devicemgmt.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/deviceio.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/display/display.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/display/wsdl/index.htm +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/display.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/doorcontrol/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/events/wsdl/bw-2-vs-mod.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/events/wsdl/event-vs.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/events/wsdl/event.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/events/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/federatedsearch.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/media/wsdl/media.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/pacs/accesscontrol.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/pacs/doorcontrol.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/pacs/types.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/provisioning/wsdl/provisioning.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/receiver.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/recording.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/replay.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/schedule/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/schedule/wsdl/schedule.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/schema/common.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/schema/metadatastream.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/schema/onvif.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/search.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/thermal/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/thermal/wsdl/thermal.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/topics/topicns.xml +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/uplink/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver10/uplink/wsdl/uplink.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/humanbody.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/humanface.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/radiometry.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/rules.xsd +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/wsdl/analytics.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/analytics/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/imaging/wsdl/imaging.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/media/wsdl/index.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/media/wsdl/media.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/ptz/wsdl/ptz.wsdl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/util/federated_search_documentation.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/util/fedsearch-docu-viewer.xsl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/util/onvif-wsdl-viewer.xsl +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif/wsdl/ver20/util/operationIndex.html +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif_python.egg-info/dependency_links.txt +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif_python.egg-info/entry_points.txt +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/onvif_python.egg-info/top_level.txt +0 -0
- {onvif_python-0.2.11 → onvif_python-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025 Nirsimetri Technologies®
|
|
3
|
+
Copyright (c) 2025-2026 Nirsimetri Technologies®
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: onvif-python
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A modern Python library for ONVIF-compliant devices
|
|
5
|
+
Author-email: Nirsimetri Technologies® <open@nirsimetri.com>
|
|
6
|
+
Maintainer: Kaburagi
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nirsimetri/onvif-python
|
|
9
|
+
Project-URL: Documentation, https://deepwiki.com/nirsimetri/onvif-python
|
|
10
|
+
Project-URL: Changelog, https://github.com/nirsimetri/onvif-python/releases
|
|
11
|
+
Keywords: onvif,Camera,onvif-python,ip-camera,cctv,soap,surveillance,cctv cameras,soap-client,onvif-client,onvif-library,onvif-specifications
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Intended Audience :: Information Technology
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: Security
|
|
27
|
+
Classifier: Topic :: Multimedia :: Video
|
|
28
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
29
|
+
Classifier: Topic :: Communications
|
|
30
|
+
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
31
|
+
Classifier: Natural Language :: English
|
|
32
|
+
Classifier: Natural Language :: Indonesian
|
|
33
|
+
Requires-Python: >=3.9
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE.md
|
|
36
|
+
Requires-Dist: zeep>=4.3.0
|
|
37
|
+
Requires-Dist: requests>=2.32.0
|
|
38
|
+
Requires-Dist: pyreadline3>=3.5.4; sys_platform == "win32"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
|
+
Requires-Dist: black; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy; extra == "dev"
|
|
43
|
+
Requires-Dist: isort; extra == "dev"
|
|
44
|
+
Requires-Dist: pylint; extra == "dev"
|
|
45
|
+
Requires-Dist: docformatter; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
<h1 align="center">ONVIF Python</h1>
|
|
49
|
+
|
|
50
|
+
<div align="center">
|
|
51
|
+
<a href="https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"><img src="https://img.shields.io/codacy/grade/bff08a94e4d447b690cea49c6594826d?style=plastic&logo=codacy&label=Code%20Quality"/></a>
|
|
52
|
+
<a href="https://pypi.org/project/onvif-python/"><img alt="PyPI Version" src="https://img.shields.io/badge/Version-0.3.1-orange?logo=python&logoColor=white&color=yellow&style=plastic"></a>
|
|
53
|
+
<a href="https://pepy.tech/projects/onvif-python"><img alt="Pepy Total Downloads" src="https://img.shields.io/pepy/dt/onvif-python?logo=pypi&logoColor=white&label=Downloads&color=red&style=plastic"></a>
|
|
54
|
+
<br>
|
|
55
|
+
<a href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml"><img alt="Build" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml/badge.svg?branch=main"></a>
|
|
56
|
+
<a href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml"><img alt="Upload Python Package" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml/badge.svg"></a>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<h1 align="center">
|
|
60
|
+
<img src="https://raw.githubusercontent.com/nirsimetri/onvif-python/refs/heads/main/assets/images/carbon_onvif.png" alt="onvif" width="700px">
|
|
61
|
+
<br>
|
|
62
|
+
</h1>
|
|
63
|
+
|
|
64
|
+
**This project provides a comprehensive and developer-friendly Python library for working with ONVIF-compliant devices.** It is designed to be reliable, easy to integrate, and flexible enough to support a wide range of ONVIF profiles and services.
|
|
65
|
+
|
|
66
|
+
**[ONVIF](https://www.onvif.org) (Open Network Video Interface Forum)** is a global standard for the interface of IP-based physical security products, including network cameras, video recorders, and related systems.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
## Who Is It For?
|
|
73
|
+
- **Individual developers** exploring ONVIF or building hobby projects
|
|
74
|
+
- **Companies** building video intelligence, analytics, or VMS platforms
|
|
75
|
+
- **Security integrators** who need reliable ONVIF interoperability across devices
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- **Python**: 3.9 or higher
|
|
80
|
+
- **Dependencies**:
|
|
81
|
+
- [`zeep>=4.3.0`](https://github.com/mvantellingen/python-zeep) - SOAP client for ONVIF communication
|
|
82
|
+
- [`requests>=2.32.0`](https://github.com/psf/requests) - HTTP library for network requests
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
From official [PyPI](https://pypi.org/project/onvif-python/):
|
|
87
|
+
```bash
|
|
88
|
+
pip install --upgrade onvif-python
|
|
89
|
+
```
|
|
90
|
+
Or clone this repository and install locally:
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/nirsimetri/onvif-python
|
|
93
|
+
cd onvif-python
|
|
94
|
+
pip install .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Documentation
|
|
98
|
+
|
|
99
|
+
For complete guide, including usage examples and API references, please visit the [Documentation Page](https://nirsimetri.github.io/onvif-python/).
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
This project is licensed under the MIT License. See [LICENSE](./LICENSE.md) for details.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<h1 align="center">ONVIF Python</h1>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"><img src="https://img.shields.io/codacy/grade/bff08a94e4d447b690cea49c6594826d?style=plastic&logo=codacy&label=Code%20Quality"/></a>
|
|
5
|
+
<a href="https://pypi.org/project/onvif-python/"><img alt="PyPI Version" src="https://img.shields.io/badge/Version-0.3.1-orange?logo=python&logoColor=white&color=yellow&style=plastic"></a>
|
|
6
|
+
<a href="https://pepy.tech/projects/onvif-python"><img alt="Pepy Total Downloads" src="https://img.shields.io/pepy/dt/onvif-python?logo=pypi&logoColor=white&label=Downloads&color=red&style=plastic"></a>
|
|
7
|
+
<br>
|
|
8
|
+
<a href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml"><img alt="Build" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml/badge.svg?branch=main"></a>
|
|
9
|
+
<a href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml"><img alt="Upload Python Package" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml/badge.svg"></a>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<h1 align="center">
|
|
13
|
+
<img src="https://raw.githubusercontent.com/nirsimetri/onvif-python/refs/heads/main/assets/images/carbon_onvif.png" alt="onvif" width="700px">
|
|
14
|
+
<br>
|
|
15
|
+
</h1>
|
|
16
|
+
|
|
17
|
+
**This project provides a comprehensive and developer-friendly Python library for working with ONVIF-compliant devices.** It is designed to be reliable, easy to integrate, and flexible enough to support a wide range of ONVIF profiles and services.
|
|
18
|
+
|
|
19
|
+
**[ONVIF](https://www.onvif.org) (Open Network Video Interface Forum)** is a global standard for the interface of IP-based physical security products, including network cameras, video recorders, and related systems.
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
## Who Is It For?
|
|
26
|
+
- **Individual developers** exploring ONVIF or building hobby projects
|
|
27
|
+
- **Companies** building video intelligence, analytics, or VMS platforms
|
|
28
|
+
- **Security integrators** who need reliable ONVIF interoperability across devices
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- **Python**: 3.9 or higher
|
|
33
|
+
- **Dependencies**:
|
|
34
|
+
- [`zeep>=4.3.0`](https://github.com/mvantellingen/python-zeep) - SOAP client for ONVIF communication
|
|
35
|
+
- [`requests>=2.32.0`](https://github.com/psf/requests) - HTTP library for network requests
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
From official [PyPI](https://pypi.org/project/onvif-python/):
|
|
40
|
+
```bash
|
|
41
|
+
pip install --upgrade onvif-python
|
|
42
|
+
```
|
|
43
|
+
Or clone this repository and install locally:
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/nirsimetri/onvif-python
|
|
46
|
+
cd onvif-python
|
|
47
|
+
pip install .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
For complete guide, including usage examples and API references, please visit the [Documentation Page](https://nirsimetri.github.io/onvif-python/).
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
This project is licensed under the MIT License. See [LICENSE](./LICENSE.md) for details.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ONVIF Python."""
|
|
2
|
+
|
|
3
|
+
from onvif.cli import main as ONVIFCLI
|
|
4
|
+
from onvif.client import ONVIFClient
|
|
5
|
+
from onvif.operator import CacheMode
|
|
6
|
+
from onvif.utils import (
|
|
7
|
+
ONVIFWSDL,
|
|
8
|
+
ONVIFDiscovery,
|
|
9
|
+
ONVIFErrorHandler,
|
|
10
|
+
ONVIFOperationException,
|
|
11
|
+
ONVIFParser,
|
|
12
|
+
ZeepPatcher,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ONVIFClient",
|
|
17
|
+
"CacheMode",
|
|
18
|
+
"ONVIFWSDL",
|
|
19
|
+
"ONVIFOperationException",
|
|
20
|
+
"ONVIFErrorHandler",
|
|
21
|
+
"ZeepPatcher",
|
|
22
|
+
"ONVIFCLI",
|
|
23
|
+
"ONVIFDiscovery",
|
|
24
|
+
"ONVIFParser",
|
|
25
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ONVIF CLI helpers."""
|
|
2
|
+
|
|
3
|
+
from onvif.cli.helpers.discovery import discover_devices, select_device_interactive
|
|
4
|
+
from onvif.cli.helpers.execution import execute_command, setup_warning_format
|
|
5
|
+
from onvif.cli.helpers.files import save_output_to_file
|
|
6
|
+
from onvif.cli.helpers.products import search_products
|
|
7
|
+
from onvif.cli.helpers.wsdl_parser import (
|
|
8
|
+
get_method_documentation,
|
|
9
|
+
get_operation_type_info,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"search_products",
|
|
14
|
+
"save_output_to_file",
|
|
15
|
+
"discover_devices",
|
|
16
|
+
"select_device_interactive",
|
|
17
|
+
"execute_command",
|
|
18
|
+
"setup_warning_format",
|
|
19
|
+
"get_method_documentation",
|
|
20
|
+
"get_operation_type_info",
|
|
21
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""ONVIF discovery helpers."""
|
|
2
|
+
|
|
3
|
+
from onvif.cli.utils import colorize
|
|
4
|
+
from onvif.utils import ONVIFDiscovery
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def discover_devices(
|
|
8
|
+
timeout: int = 4,
|
|
9
|
+
interface: str | None = None,
|
|
10
|
+
prefer_https: bool = False,
|
|
11
|
+
filter_term: str | None = None,
|
|
12
|
+
) -> list:
|
|
13
|
+
"""Discover ONVIF devices on the network using WS-Discovery.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
timeout (int): Discovery timeout in seconds
|
|
17
|
+
interface (str | None): Network interface to use for discovery
|
|
18
|
+
prefer_https (bool): If True, prioritize HTTPS XAddrs when available
|
|
19
|
+
filter_term (str | None): Optional search term to filter devices by types or scopes
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of discovered devices with connection info
|
|
23
|
+
"""
|
|
24
|
+
# Use ONVIFDiscovery class
|
|
25
|
+
discovery = ONVIFDiscovery(timeout=timeout, interface=interface)
|
|
26
|
+
|
|
27
|
+
print(f"\n{colorize('Discovering ONVIF devices on network...', 'yellow')}")
|
|
28
|
+
print(f"Network interface: {colorize(discovery.get_local_ip(), 'white')}")
|
|
29
|
+
print(f"Timeout: {timeout}s")
|
|
30
|
+
if filter_term:
|
|
31
|
+
print(f"Filter: {colorize(filter_term, 'yellow')}")
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
devices = discovery.discover(prefer_https=prefer_https, search=filter_term)
|
|
35
|
+
|
|
36
|
+
return devices
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
|
|
40
|
+
"""Display devices and allow user to select one interactively.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
devices (list): Discovered devices
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple of (host, port, use_https) or None if cancelled
|
|
47
|
+
"""
|
|
48
|
+
if not devices:
|
|
49
|
+
print(f"\n{colorize('No ONVIF devices found.', 'red')}")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
print(f"{colorize(f'Found {len(devices)} ONVIF device(s):', 'green')}")
|
|
53
|
+
|
|
54
|
+
for idx, device in enumerate(devices, 1):
|
|
55
|
+
idx_str = colorize(f"[{idx}]", "yellow")
|
|
56
|
+
protocol = "https" if device.get("use_https", False) else "http"
|
|
57
|
+
host_port = f"{device['host']}:{device['port']}"
|
|
58
|
+
protocol_indicator = (
|
|
59
|
+
colorize("🔒 HTTPS", "green")
|
|
60
|
+
if device.get("use_https", False)
|
|
61
|
+
else colorize("HTTP", "white")
|
|
62
|
+
)
|
|
63
|
+
print(f"\n{idx_str} {colorize(host_port, 'yellow')} ({protocol_indicator})")
|
|
64
|
+
|
|
65
|
+
# Remove uuid: or urn:uuid: prefix from EPR
|
|
66
|
+
epr_display = device["epr"]
|
|
67
|
+
if epr_display.startswith("urn:uuid:"):
|
|
68
|
+
epr_display = epr_display.replace("urn:uuid:", "")
|
|
69
|
+
elif epr_display.startswith("uuid:"):
|
|
70
|
+
epr_display = epr_display.replace("uuid:", "")
|
|
71
|
+
print(f" [uuid] {epr_display}")
|
|
72
|
+
|
|
73
|
+
if device["xaddrs"]:
|
|
74
|
+
xaddrs_parts = [f"[{xaddr}]" for xaddr in device["xaddrs"]]
|
|
75
|
+
print(f" [xaddrs] {' '.join(xaddrs_parts)}")
|
|
76
|
+
|
|
77
|
+
if device["types"]:
|
|
78
|
+
types_parts = [f"[{t}]" for t in device["types"]]
|
|
79
|
+
print(f" [types] {' '.join(types_parts)}")
|
|
80
|
+
|
|
81
|
+
if device["scopes"]:
|
|
82
|
+
scope_parts = []
|
|
83
|
+
for scope in device["scopes"]:
|
|
84
|
+
# Remove the prefix "onvif://www.onvif.org/" if present
|
|
85
|
+
if scope.startswith("onvif://www.onvif.org/"):
|
|
86
|
+
simplified = scope.replace("onvif://www.onvif.org/", "")
|
|
87
|
+
scope_parts.append(f"[{simplified}]")
|
|
88
|
+
else:
|
|
89
|
+
# Keep other scopes as-is (e.g., http:123)
|
|
90
|
+
scope_parts.append(f"[{scope}]")
|
|
91
|
+
|
|
92
|
+
if scope_parts:
|
|
93
|
+
print(f" [scopes] {' '.join(scope_parts)}")
|
|
94
|
+
|
|
95
|
+
# Simple selection (without arrow keys for cross-platform compatibility)
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
selection = input(
|
|
99
|
+
f"\nSelect device number {colorize(f'1-{len(devices)}', 'white')} or {colorize('q', 'white')} to quit: "
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if selection.lower() == "q":
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
idx = int(selection)
|
|
106
|
+
if 1 <= idx <= len(devices):
|
|
107
|
+
selected = devices[idx - 1]
|
|
108
|
+
protocol = "https" if selected.get("use_https", False) else "http"
|
|
109
|
+
host_port = f"{selected['host']}:{selected['port']}"
|
|
110
|
+
print(
|
|
111
|
+
f"\n{colorize('Selected:', 'green')} {colorize(protocol, 'cyan')}://{colorize(host_port, 'yellow')}"
|
|
112
|
+
)
|
|
113
|
+
return (
|
|
114
|
+
selected["host"],
|
|
115
|
+
selected["port"],
|
|
116
|
+
selected.get("use_https", False),
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
print(colorize("Invalid selection. Please try again.", "red"))
|
|
120
|
+
|
|
121
|
+
except ValueError:
|
|
122
|
+
print(colorize("Invalid input. Please enter a number.", "red"))
|
|
123
|
+
except (EOFError, KeyboardInterrupt):
|
|
124
|
+
return None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""ONVIF command execution helpers."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from onvif.cli.utils import colorize, parse_json_params
|
|
7
|
+
from onvif.client import ONVIFClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute_command(
|
|
11
|
+
client: ONVIFClient,
|
|
12
|
+
service_name: str,
|
|
13
|
+
method_name: str,
|
|
14
|
+
params_str: str | None = None,
|
|
15
|
+
) -> Any:
|
|
16
|
+
"""Execute a single ONVIF command.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
client (ONVIFClient): ONVIFClient instance
|
|
20
|
+
service_name (str): Selected service for command execution
|
|
21
|
+
method_name (str): Method to execute by service
|
|
22
|
+
params_str (str | None): Provided params for method execution
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The operation response printed in terminal
|
|
26
|
+
"""
|
|
27
|
+
# Get service instance
|
|
28
|
+
try:
|
|
29
|
+
service = getattr(client, service_name.lower())()
|
|
30
|
+
except AttributeError as e:
|
|
31
|
+
raise ValueError(f"{colorize('Unknown service:', 'red')} {service_name}") from e
|
|
32
|
+
|
|
33
|
+
# Get method
|
|
34
|
+
try:
|
|
35
|
+
method = getattr(service, method_name)
|
|
36
|
+
except AttributeError as e:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"{colorize('Unknown method', 'red')} '{method_name}' for service '{service_name}'"
|
|
39
|
+
) from e
|
|
40
|
+
|
|
41
|
+
# Parse parameters
|
|
42
|
+
params = parse_json_params(params_str) if params_str else {}
|
|
43
|
+
|
|
44
|
+
# Execute method
|
|
45
|
+
return method(**params)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def setup_warning_format() -> None:
|
|
49
|
+
"""Setup custom warning format to show clean, concise warnings."""
|
|
50
|
+
|
|
51
|
+
def custom_warning_format(
|
|
52
|
+
message, category, filename, lineno, line=None
|
|
53
|
+
): # pylint: disable=unused-argument
|
|
54
|
+
# Show only the warning message without file path and line number
|
|
55
|
+
return f"{category.__name__}: {message}\n"
|
|
56
|
+
|
|
57
|
+
warnings.formatwarning = custom_warning_format
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""File processing helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from onvif.cli.utils import colorize
|
|
10
|
+
from onvif.client import ONVIFClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_output_to_file(
|
|
14
|
+
result: Any, output_path: str, debug_mode: bool, client: ONVIFClient
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Save command output to file in appropriate format based on file extension.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
result (Any): The ONVIF command result
|
|
20
|
+
output_path (str): Path to output file
|
|
21
|
+
debug_mode (bool): Whether debug mode is enabled (for XML capture)
|
|
22
|
+
client (ONVIFClient): ONVIFClient instance (for accessing XML plugin)
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
# Determine output format based on file extension
|
|
26
|
+
_, ext = os.path.splitext(output_path.lower())
|
|
27
|
+
|
|
28
|
+
if ext == ".json":
|
|
29
|
+
# Prepare output data
|
|
30
|
+
output_data = {}
|
|
31
|
+
|
|
32
|
+
# JSON format
|
|
33
|
+
output_data["result"] = _serialize_for_json(result)
|
|
34
|
+
output_data["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
35
|
+
output_data["raw_result"] = str(result) # Add raw string as fallback
|
|
36
|
+
|
|
37
|
+
# Add XML data if debug mode is enabled and XML plugin is available
|
|
38
|
+
if debug_mode and client.xml_plugin:
|
|
39
|
+
output_data["debug"] = {
|
|
40
|
+
"last_request_xml": client.xml_plugin.last_sent_xml,
|
|
41
|
+
"last_response_xml": client.xml_plugin.last_received_xml,
|
|
42
|
+
"last_operation": client.xml_plugin.last_operation,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
46
|
+
json.dump(output_data, f, indent=2, ensure_ascii=False)
|
|
47
|
+
|
|
48
|
+
elif ext == ".xml":
|
|
49
|
+
# XML format - prioritize raw SOAP XML over parsed result
|
|
50
|
+
if client.xml_plugin and client.xml_plugin.last_received_xml:
|
|
51
|
+
# Save the raw SOAP response XML with minimal wrapper
|
|
52
|
+
content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<!-- ONVIF SOAP Response -->
|
|
54
|
+
<!-- Timestamp: {datetime.now(timezone.utc).isoformat()} -->
|
|
55
|
+
<!-- Operation: {client.xml_plugin.last_operation or 'Unknown'} -->
|
|
56
|
+
|
|
57
|
+
{client.xml_plugin.last_received_xml}
|
|
58
|
+
"""
|
|
59
|
+
else:
|
|
60
|
+
# Fallback: Simple XML wrapper for the parsed result
|
|
61
|
+
content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
62
|
+
<!-- ONVIF Command Output (Parsed Result) -->
|
|
63
|
+
<!-- Timestamp: {datetime.now(timezone.utc).isoformat()} -->
|
|
64
|
+
<!-- Note: Raw SOAP XML not available. Enable --debug for full SOAP capture. -->
|
|
65
|
+
<onvif_result>
|
|
66
|
+
<![CDATA[
|
|
67
|
+
{result!s}
|
|
68
|
+
]]>
|
|
69
|
+
</onvif_result>
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
73
|
+
f.write(content)
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
# Plain text format (default)
|
|
77
|
+
content = "ONVIF Command Output\n"
|
|
78
|
+
content += f"Timestamp: {datetime.now(timezone.utc).isoformat()}\n"
|
|
79
|
+
content += f"{'='*50}\n\n"
|
|
80
|
+
content += str(result)
|
|
81
|
+
|
|
82
|
+
# Add debug information if available
|
|
83
|
+
if debug_mode and client.xml_plugin:
|
|
84
|
+
content += f"\n\n{'='*50}\n"
|
|
85
|
+
content += "DEBUG INFORMATION\n"
|
|
86
|
+
content += f"{'='*50}\n"
|
|
87
|
+
if client.xml_plugin.last_operation:
|
|
88
|
+
content += f"Operation: {client.xml_plugin.last_operation}\n\n"
|
|
89
|
+
if client.xml_plugin.last_sent_xml:
|
|
90
|
+
content += "SOAP Request:\n"
|
|
91
|
+
content += client.xml_plugin.last_sent_xml + "\n\n"
|
|
92
|
+
if client.xml_plugin.last_received_xml:
|
|
93
|
+
content += "SOAP Response:\n"
|
|
94
|
+
content += client.xml_plugin.last_received_xml + "\n"
|
|
95
|
+
|
|
96
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
97
|
+
f.write(content)
|
|
98
|
+
|
|
99
|
+
except (AttributeError, ValueError, OSError) as e:
|
|
100
|
+
print(f"{colorize('Error saving output:', 'red')} {e}", file=sys.stderr)
|
|
101
|
+
# Still print the result to console if file save fails
|
|
102
|
+
print(str(result))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _serialize_for_json(obj: Any) -> Any:
|
|
106
|
+
"""Recursively serialize ONVIF objects for JSON output.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
obj: Object to serialize
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
JSON-serializable representation of the object
|
|
113
|
+
"""
|
|
114
|
+
if obj is None:
|
|
115
|
+
return None
|
|
116
|
+
elif isinstance(obj, (str, int, float, bool)):
|
|
117
|
+
return obj
|
|
118
|
+
elif isinstance(obj, datetime):
|
|
119
|
+
return obj.isoformat()
|
|
120
|
+
elif isinstance(obj, (list, tuple)):
|
|
121
|
+
return [_serialize_for_json(item) for item in obj]
|
|
122
|
+
elif isinstance(obj, dict):
|
|
123
|
+
return {key: _serialize_for_json(value) for key, value in obj.items()}
|
|
124
|
+
|
|
125
|
+
# Check if this is a Zeep object (has _xsd_type attribute)
|
|
126
|
+
elif hasattr(obj, "_xsd_type"):
|
|
127
|
+
result = {}
|
|
128
|
+
# Try to get all elements from XSD type
|
|
129
|
+
if hasattr(obj._xsd_type, "elements"): # pylint: disable=protected-access
|
|
130
|
+
for (
|
|
131
|
+
elem_name,
|
|
132
|
+
_,
|
|
133
|
+
) in obj._xsd_type.elements: # pylint: disable=protected-access
|
|
134
|
+
try:
|
|
135
|
+
value = getattr(obj, elem_name, None)
|
|
136
|
+
if value is not None:
|
|
137
|
+
result[elem_name] = _serialize_for_json(value)
|
|
138
|
+
except (AttributeError, TypeError):
|
|
139
|
+
# Skip elements that can't be accessed or have type issues
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# Also try regular attributes
|
|
143
|
+
for attr_name in dir(obj):
|
|
144
|
+
if not attr_name.startswith("_") and not callable(
|
|
145
|
+
getattr(obj, attr_name, None)
|
|
146
|
+
):
|
|
147
|
+
try:
|
|
148
|
+
attr_value = getattr(obj, attr_name)
|
|
149
|
+
if attr_value is not None and attr_name not in result:
|
|
150
|
+
result[attr_name] = _serialize_for_json(attr_value)
|
|
151
|
+
except (AttributeError, TypeError):
|
|
152
|
+
# Skip attributes that can't be accessed or have type issues
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
elif hasattr(obj, "__dict__"):
|
|
158
|
+
# Handle regular objects with attributes
|
|
159
|
+
result = {}
|
|
160
|
+
for key, value in obj.__dict__.items():
|
|
161
|
+
if not key.startswith("_"): # Skip private attributes
|
|
162
|
+
result[key] = _serialize_for_json(value)
|
|
163
|
+
|
|
164
|
+
# If result is empty, try to get attributes using dir()
|
|
165
|
+
if not result:
|
|
166
|
+
for attr_name in dir(obj):
|
|
167
|
+
if not attr_name.startswith("_") and not callable(
|
|
168
|
+
getattr(obj, attr_name, None)
|
|
169
|
+
):
|
|
170
|
+
try:
|
|
171
|
+
attr_value = getattr(obj, attr_name)
|
|
172
|
+
if attr_value is not None:
|
|
173
|
+
result[attr_name] = _serialize_for_json(attr_value)
|
|
174
|
+
except (AttributeError, TypeError):
|
|
175
|
+
# Skip attributes that can't be accessed or have type issues
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
elif hasattr(obj, "_value_1"):
|
|
180
|
+
# Handle zeep objects with special structure
|
|
181
|
+
return _serialize_for_json(obj._value_1) # pylint: disable=protected-access
|
|
182
|
+
else:
|
|
183
|
+
# Try to convert to dict using vars() if available
|
|
184
|
+
try:
|
|
185
|
+
obj_dict = vars(obj)
|
|
186
|
+
return _serialize_for_json(obj_dict)
|
|
187
|
+
except TypeError:
|
|
188
|
+
# Fallback to string representation
|
|
189
|
+
return str(obj)
|