From 4a11e13e1337136303e5ce3c57ed15d8cd446b37 Mon Sep 17 00:00:00 2001 From: Jeff Terrace Date: Thu, 21 Nov 2024 18:06:46 -0500 Subject: [PATCH 1/4] Add shortened common onvif namespace prefixes. This fixes some Tapo cameras that respond with a 500 error when receiving the ns0/ns1 namespace prefix with embedded xmlns definitions inside each element. A similar change was done upstream here: https://github.com/FalkTannhaeuser/python-onvif-zeep/pull/46 for https://github.com/mvantellingen/python-zeep/issues/870. I also updated the events example that was broken. --- examples/events.py | 130 +++++++++++++++++++++++++++++++-------------- onvif/client.py | 6 +++ requirements.txt | 6 ++- 3 files changed, 101 insertions(+), 41 deletions(-) diff --git a/examples/events.py b/examples/events.py index bcddf9f..420060c 100644 --- a/examples/events.py +++ b/examples/events.py @@ -1,55 +1,107 @@ """Example to fetch pullpoint events.""" + +from aiohttp import web +import argparse import asyncio import datetime as dt import logging +import onvif +import os.path +import pprint +import sys + +SUBSCRIPTION_TIME = dt.timedelta(minutes=1) +WAIT_TIME = dt.timedelta(seconds=30) + -from pytz import UTC -from zeep import xsd +def subscription_lost(): + print("subscription lost") -from onvif import ONVIFCamera -logging.getLogger("zeep").setLevel(logging.DEBUG) +async def post_handler(request): + print(request) + print(request.url) + for k, v in request.headers.items(): + print(f"{k}: {v}") + body = await request.content.read() + print(body) + return web.Response() -async def run(): - mycam = ONVIFCamera( - "192.168.3.10", - 80, - "hass", - "peek4boo", - wsdl_dir="/home/jason/python-onvif-zeep-async/onvif/wsdl", +async def run(args): + mycam = onvif.ONVIFCamera( + args.host, + args.port, + args.username, + args.password, + wsdl_dir=f"{os.path.dirname(onvif.__file__)}/wsdl/", ) await mycam.update_xaddrs() - if not await mycam.create_pullpoint_subscription(): - print("PullPoint not supported") - return - - event_service = mycam.create_events_service() - properties = await event_service.GetEventProperties() - print(properties) - capabilities = await event_service.GetServiceCapabilities() - print(capabilities) - - pullpoint = mycam.create_pullpoint_service() - await pullpoint.SetSynchronizationPoint() - req = pullpoint.create_type("PullMessages") - req.MessageLimit = 100 - req.Timeout = dt.timedelta(seconds=30) - messages = await pullpoint.PullMessages(req) - print(messages) - - subscription = mycam.create_subscription_service("PullPointSubscription") - termination_time = ( - (dt.datetime.utcnow() + dt.timedelta(days=1)) - .isoformat(timespec="seconds") - .replace("+00:00", "Z") - ) - await subscription.Renew(termination_time) - await subscription.Unsubscribe() + capabilities = await mycam.get_capabilities() + pprint.pprint(capabilities) + + if args.notification: + app = web.Application() + app.add_routes([web.post("/", post_handler)]) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, args.notification_address, args.notification_port) + await site.start() + + receive_url = f"http://{args.notification_address}:{args.notification_port}/" + manager = await mycam.create_notification_manager( + receive_url, + SUBSCRIPTION_TIME, + subscription_lost, + ) + await manager.set_synchronization_point() + + print(f"waiting for messages at {receive_url}...") + await asyncio.sleep(WAIT_TIME.total_seconds()) + + await manager.shutdown() + await runner.cleanup() + else: + manager = await mycam.create_pullpoint_manager( + SUBSCRIPTION_TIME, subscription_lost + ) + await manager.set_synchronization_point() + + pullpoint = manager.get_service() + print("waiting for messages...") + messages = await pullpoint.PullMessages( + { + "MessageLimit": 100, + "Timeout": WAIT_TIME, + } + ) + print(messages) + + await manager.shutdown() + await mycam.close() -if __name__ == "__main__": +def main(): + logging.getLogger("zeep").setLevel(logging.DEBUG) + + parser = argparse.ArgumentParser(prog="EventTester") + parser.add_argument("--host", default="192.168.3.10") + parser.add_argument("--port", type=int, default=80) + parser.add_argument("--username", default="hass") + parser.add_argument("--password", default="peek4boo") + parser.add_argument("--notification", action=argparse.BooleanOptionalAction) + parser.add_argument("--notification_address") + parser.add_argument("--notification_port", type=int, default=8976) + + args = parser.parse_args(sys.argv[1:]) + if args.notification and args.notification_address is None: + parser.error("--notification requires --notification_address") + loop = asyncio.get_event_loop() - loop.run_until_complete(run()) + loop.run_until_complete(run(args)) + + +if __name__ == "__main__": + main() diff --git a/onvif/client.py b/onvif/client.py index 8f56941..0bc69f1 100644 --- a/onvif/client.py +++ b/onvif/client.py @@ -1,4 +1,5 @@ """ONVIF Client.""" + from __future__ import annotations import asyncio @@ -125,6 +126,11 @@ def _load_document() -> DocumentWithDeferredLoad: class ZeepAsyncClient(BaseZeepAsyncClient): """Overwrite create_service method to be async.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_ns_prefix('wsnt', 'http://docs.oasis-open.org/wsn/b-2') + self.set_ns_prefix('wsa', 'http://www.w3.org/2005/08/addressing') + def create_service(self, binding_name, address): """Create a new ServiceProxy for the given binding name and address. :param binding_name: The QName of the binding diff --git a/requirements.txt b/requirements.txt index 43ac312..b710a23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ # Package -httpx==0.19.0 -zeep[async]==4.1.0 +ciso8601>=2.1.3 +httpx>=0.19.0,<1.0.0 +zeep[async]>=4.2.1,<5.0.0 # Dev +aiohttp pytest pytest-cov pylint From bd4cad0539406f2d8f6390d2ccbd05a5e034b7bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:28:26 +0000 Subject: [PATCH 2/4] chore(pre-commit.ci): auto fixes --- onvif/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onvif/client.py b/onvif/client.py index 0bc69f1..02c155c 100644 --- a/onvif/client.py +++ b/onvif/client.py @@ -128,8 +128,8 @@ class ZeepAsyncClient(BaseZeepAsyncClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_ns_prefix('wsnt', 'http://docs.oasis-open.org/wsn/b-2') - self.set_ns_prefix('wsa', 'http://www.w3.org/2005/08/addressing') + self.set_ns_prefix("wsnt", "http://docs.oasis-open.org/wsn/b-2") + self.set_ns_prefix("wsa", "http://www.w3.org/2005/08/addressing") def create_service(self, binding_name, address): """Create a new ServiceProxy for the given binding name and address. From 4ea62e8711c94d77fd3185aca99c4dc7fd734281 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Dec 2024 15:56:28 -0600 Subject: [PATCH 3/4] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4bcd1ce..8fd3a20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # Package -ciso8601>=2.1.3 +ciso8601==2.1.3 httpx==0.28.0 zeep[async]==4.3.1 From 927df8db76a3002074c822adf012ebc7ba2cf3dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Dec 2024 16:28:10 -0600 Subject: [PATCH 4/4] fix examples --- requirements_dev.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index eaa59b5..09e4845 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,6 +1,9 @@ # Package -r requirements.txt +# Examples +aiohttp==3.11.9 + # Dev pytest==8.3.4 pytest-cov==6.0.0