This repository has been archived on 2023-06-10. You can view files and clone it, but cannot push or open issues or pull requests.
homeassistant-config/deps/lib/python3.10/site-packages/onvif/client.py

484 lines
16 KiB
Python
Raw Normal View History

2023-02-11 18:37:07 +00:00
"""ONVIF Client."""
import datetime as dt
import logging
import os.path
import httpx
from httpx import AsyncClient, BasicAuth, DigestAuth
from zeep.cache import SqliteCache
from zeep.client import AsyncClient as BaseZeepAsyncClient, Client, Settings
from zeep.exceptions import Fault
import zeep.helpers
from zeep.proxy import AsyncServiceProxy
from zeep.transports import AsyncTransport
from zeep.wsse.username import UsernameToken
from onvif.definition import SERVICES
from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError
logger = logging.getLogger("onvif")
logging.basicConfig(level=logging.INFO)
logging.getLogger("zeep.client").setLevel(logging.CRITICAL)
def safe_func(func):
"""Ensure methods to raise an ONVIFError Exception when some thing was wrong."""
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as err:
raise ONVIFError(err)
return wrapped
class UsernameDigestTokenDtDiff(UsernameToken):
"""
UsernameDigestToken class, with a time offset parameter that can be adjusted;
This allows authentication on cameras without being time synchronized.
Please note that using NTP on both end is the recommended solution,
this should only be used in "safe" environments.
"""
def __init__(self, user, passw, dt_diff=None, **kwargs):
super().__init__(user, passw, **kwargs)
# Date/time difference in datetime.timedelta
self.dt_diff = dt_diff
def apply(self, envelope, headers):
old_created = self.created
if self.created is None:
self.created = dt.datetime.utcnow()
if self.dt_diff is not None:
self.created += self.dt_diff
result = super().apply(envelope, headers)
self.created = old_created
return result
class ZeepAsyncClient(BaseZeepAsyncClient):
"""Overwrite create_service method to be async."""
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
:param address: The address of the endpoint
"""
try:
binding = self.wsdl.bindings[binding_name]
except KeyError:
raise ValueError(
"No binding found with the given QName. Available bindings "
"are: %s" % (", ".join(self.wsdl.bindings.keys()))
)
return AsyncServiceProxy(self, binding, address=address)
class ONVIFService:
"""
Python Implemention for ONVIF Service.
Services List:
DeviceMgmt DeviceIO Event AnalyticsDevice Display Imaging Media
PTZ Receiver RemoteDiscovery Recording Replay Search Extension
>>> from onvif import ONVIFService
>>> device_service = ONVIFService('http://192.168.0.112/onvif/device_service',
... 'admin', 'foscam',
... '/etc/onvif/wsdl/devicemgmt.wsdl')
>>> ret = device_service.GetHostname()
>>> print ret.FromDHCP
>>> print ret.Name
>>> device_service.SetHostname(dict(Name='newhostname'))
>>> ret = device_service.GetSystemDateAndTime()
>>> print ret.DaylightSavings
>>> print ret.TimeZone
>>> dict_ret = device_service.to_dict(ret)
>>> print dict_ret['TimeZone']
There are two ways to pass parameter to services methods
1. Dict
params = {'Name': 'NewHostName'}
device_service.SetHostname(params)
2. Type Instance
params = device_service.create_type('SetHostname')
params.Hostname = 'NewHostName'
device_service.SetHostname(params)
"""
@safe_func
def __init__(
self,
xaddr,
user,
passwd,
url,
encrypt=True,
no_cache=False,
dt_diff=None,
binding_name="",
binding_key="",
):
if not os.path.isfile(url):
raise ONVIFError("%s doesn`t exist!" % url)
self.url = url
self.xaddr = xaddr
self.binding_key = binding_key
wsse = UsernameDigestTokenDtDiff(
user, passwd, dt_diff=dt_diff, use_digest=encrypt
)
# Create soap client
client = AsyncClient(timeout=90)
self.transport = (
AsyncTransport(client=client)
if no_cache
else AsyncTransport(client=client, cache=SqliteCache())
)
settings = Settings()
settings.strict = False
settings.xml_huge_tree = True
self.zeep_client_authless = ZeepAsyncClient(
wsdl=url,
transport=self.transport,
settings=settings
)
self.ws_client_authless = self.zeep_client_authless.create_service(binding_name, self.xaddr)
self.zeep_client = ZeepAsyncClient(
wsdl=url,
wsse=wsse,
transport=self.transport,
settings=settings
)
self.ws_client = self.zeep_client.create_service(binding_name, self.xaddr)
# Set soap header for authentication
self.user = user
self.passwd = passwd
# Indicate wether password digest is needed
self.encrypt = encrypt
self.dt_diff = dt_diff
namespace = binding_name[binding_name.find("{") + 1 : binding_name.find("}")]
available_ns = self.zeep_client.namespaces
active_ns = (
list(available_ns.keys())[list(available_ns.values()).index(namespace)]
or "ns0"
)
self.create_type = lambda x: self.zeep_client.get_element(active_ns + ":" + x)()
async def close(self):
"""Close the transport."""
await self.transport.aclose()
@staticmethod
@safe_func
def to_dict(zeepobject):
"""Convert a WSDL Type instance into a dictionary."""
return {} if zeepobject is None else zeep.helpers.serialize_object(zeepobject)
def __getattr__(self, name):
"""
Call the real onvif Service operations,
See the official wsdl definition for the
APIs detail(API name, request parameters,
response parameters, parameter types, etc...)
"""
def service_wrapper(func):
"""Wrap service call."""
@safe_func
def wrapped(params=None):
def call(params=None):
# No params
if params is None:
params = {}
else:
params = ONVIFService.to_dict(params)
try:
ret = func(**params)
except TypeError:
ret = func(params)
return ret
return call(params)
return wrapped
builtin = name.startswith("__") and name.endswith("__")
if builtin:
return self.__dict__[name]
if name.startswith("authless_"):
return service_wrapper(getattr(self.ws_client_authless, name.split("_")[1]))
return service_wrapper(getattr(self.ws_client, name))
class ONVIFCamera:
"""
Python Implemention ONVIF compliant device
This class integrates onvif services
adjust_time parameter allows authentication on cameras without being time synchronized.
Please note that using NTP on both end is the recommended solution,
this should only be used in "safe" environments.
Also, this cannot be used on AXIS camera, as every request is authenticated, contrary to ONVIF standard
>>> from onvif import ONVIFCamera
>>> mycam = ONVIFCamera('192.168.0.112', 80, 'admin', '12345')
>>> mycam.devicemgmt.GetServices(False)
>>> media_service = mycam.create_media_service()
>>> ptz_service = mycam.create_ptz_service()
# Get PTZ Configuration:
>>> mycam.ptz.GetConfiguration()
# Another way:
>>> ptz_service.GetConfiguration()
"""
def __init__(
self,
host,
port,
user,
passwd,
wsdl_dir=os.path.join(os.path.dirname(os.path.dirname(__file__)), "wsdl"),
encrypt=True,
no_cache=False,
adjust_time=False,
):
os.environ.pop("http_proxy", None)
os.environ.pop("https_proxy", None)
self.host = host
self.port = int(port)
self.user = user
self.passwd = passwd
self.wsdl_dir = wsdl_dir
self.encrypt = encrypt
self.no_cache = no_cache
self.adjust_time = adjust_time
self.dt_diff = None
self.xaddrs = {}
# Active service client container
self.services = {}
self.to_dict = ONVIFService.to_dict
self._snapshot_uris = {}
self._snapshot_client = AsyncClient()
async def update_xaddrs(self):
"""Update xaddrs for services."""
self.dt_diff = None
devicemgmt = self.create_devicemgmt_service()
if self.adjust_time:
try:
sys_date = await devicemgmt.authless_GetSystemDateAndTime()
except zeep.exceptions.Fault:
# Looks like we should try with auth
sys_date = await devicemgmt.GetSystemDateAndTime()
cdate = sys_date.UTCDateTime
cam_date = dt.datetime(
cdate.Date.Year,
cdate.Date.Month,
cdate.Date.Day,
cdate.Time.Hour,
cdate.Time.Minute,
cdate.Time.Second,
)
self.dt_diff = cam_date - dt.datetime.utcnow()
await devicemgmt.close()
del self.services[devicemgmt.binding_key]
devicemgmt = self.create_devicemgmt_service()
# Get XAddr of services on the device
self.xaddrs = {}
capabilities = await devicemgmt.GetCapabilities({"Category": "All"})
for name in capabilities:
capability = capabilities[name]
try:
if name.lower() in SERVICES and capability is not None:
namespace = SERVICES[name.lower()]["ns"]
self.xaddrs[namespace] = capability["XAddr"]
except Exception:
logger.exception("Unexpected service type")
async def create_pullpoint_subscription(self):
"""Create a pullpoint subscription."""
try:
events = self.create_events_service()
pullpoint = await events.CreatePullPointSubscription()
# pylint: disable=protected-access
self.xaddrs[
"http://www.onvif.org/ver10/events/wsdl/PullPointSubscription"
] = pullpoint.SubscriptionReference.Address._value_1
except Fault:
return False
return True
async def close(self):
"""Close all transports."""
await self._snapshot_client.aclose()
for service in self.services.values():
await service.close()
async def get_snapshot_uri(self, profile_token):
"""Get the snapshot uri for a given profile."""
uri = self._snapshot_uris.get(profile_token)
if uri is None:
media_service = self.create_media_service()
req = media_service.create_type("GetSnapshotUri")
req.ProfileToken = profile_token
result = await media_service.GetSnapshotUri(req)
uri = result.Uri
self._snapshot_uris[profile_token] = uri
return uri
async def get_snapshot(self, profile_token, basic_auth=False):
"""Get a snapshot image from the camera."""
uri = await self.get_snapshot_uri(profile_token)
if uri is None:
return None
auth = None
if self.user and self.passwd:
if basic_auth:
auth = BasicAuth(self.user, self.passwd)
else:
auth = DigestAuth(self.user, self.passwd)
try:
response = await self._snapshot_client.get(uri, auth=auth)
except httpx.TimeoutException as error:
raise ONVIFTimeoutError(error) from error
except httpx.RequestError as error:
raise ONVIFError(error) from error
if response.status_code == 401:
raise ONVIFAuthError(f"Failed to authenticate to {uri}")
if response.status_code < 300:
return response.content
return None
def get_definition(self, name, port_type=None):
"""Returns xaddr and wsdl of specified service"""
# Check if the service is supported
if name not in SERVICES:
raise ONVIFError("Unknown service %s" % name)
wsdl_file = SERVICES[name]["wsdl"]
namespace = SERVICES[name]["ns"]
binding_name = "{{{}}}{}".format(namespace, SERVICES[name]["binding"])
if port_type:
namespace += "/" + port_type
wsdlpath = os.path.join(self.wsdl_dir, wsdl_file)
if not os.path.isfile(wsdlpath):
raise ONVIFError("No such file: %s" % wsdlpath)
# XAddr for devicemgmt is fixed:
if name == "devicemgmt":
xaddr = "{}:{}/onvif/device_service".format(
self.host
if (self.host.startswith("http://") or self.host.startswith("https://"))
else "http://%s" % self.host,
self.port,
)
return xaddr, wsdlpath, binding_name
# Get other XAddr
xaddr = self.xaddrs.get(namespace)
if not xaddr:
raise ONVIFError("Device doesn`t support service: %s" % name)
return xaddr, wsdlpath, binding_name
def create_onvif_service(self, name, port_type=None):
"""Create ONVIF service client"""
name = name.lower()
xaddr, wsdl_file, binding_name = self.get_definition(name, port_type)
# Don't re-create bindings if the xaddr remains the same.
# The xaddr can change when a new PullPointSubscription is created.
binding_key = f"{binding_name}{xaddr}"
binding = self.services.get(binding_key)
if binding:
return binding
service = ONVIFService(
xaddr,
self.user,
self.passwd,
wsdl_file,
self.encrypt,
no_cache=self.no_cache,
dt_diff=self.dt_diff,
binding_name=binding_name,
binding_key=binding_key
)
self.services[binding_key] = service
return service
def create_devicemgmt_service(self):
"""Service creation helper."""
return self.create_onvif_service("devicemgmt")
def create_media_service(self):
"""Service creation helper."""
return self.create_onvif_service("media")
def create_ptz_service(self):
"""Service creation helper."""
return self.create_onvif_service("ptz")
def create_imaging_service(self):
"""Service creation helper."""
return self.create_onvif_service("imaging")
def create_deviceio_service(self):
"""Service creation helper."""
return self.create_onvif_service("deviceio")
def create_events_service(self):
"""Service creation helper."""
return self.create_onvif_service("events")
def create_analytics_service(self):
"""Service creation helper."""
return self.create_onvif_service("analytics")
def create_recording_service(self):
"""Service creation helper."""
return self.create_onvif_service("recording")
def create_search_service(self):
"""Service creation helper."""
return self.create_onvif_service("search")
def create_replay_service(self):
"""Service creation helper."""
return self.create_onvif_service("replay")
def create_pullpoint_service(self):
"""Service creation helper."""
return self.create_onvif_service("pullpoint", port_type="PullPointSubscription")
def create_notification_service(self):
"""Service creation helper."""
return self.create_onvif_service("notification")
def create_subscription_service(self, port_type=None):
"""Service creation helper."""
return self.create_onvif_service("subscription", port_type=port_type)
def create_receiver_service(self):
"""Service creation helper."""
return self.create_onvif_service("receiver")