Skip to content

Commit 2b998d1

Browse files
committed
Improve handling of overflowing timestamps
home-assistant/core#124086
1 parent f1fa9b3 commit 2b998d1

File tree

4 files changed

+120
-8
lines changed

4 files changed

+120
-8
lines changed

onvif/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .managers import NotificationManager, PullPointManager
2727
from .settings import DEFAULT_SETTINGS
2828
from .transport import ASYNC_TRANSPORT
29-
from .types import FastDateTime
29+
from .types import FastDateTime, ForgivingTime
3030
from .util import create_no_verify_ssl_context, normalize_url, path_isfile, utcnow
3131
from .wrappers import retry_connection_error # noqa: F401
3232
from .wsa import WsAddressingIfMissingPlugin
@@ -129,8 +129,14 @@ def _load_document() -> DocumentWithDeferredLoad:
129129
schema = document.types.documents.get_by_namespace(
130130
"http://www.w3.org/2001/XMLSchema", False
131131
)[0]
132+
logger.debug("Overriding default datetime type to use FastDateTime")
132133
instance = FastDateTime(is_global=True)
133134
schema.register_type(FastDateTime._default_qname, instance)
135+
136+
logger.debug("Overriding default time type to use ForgivingTime")
137+
instance = ForgivingTime(is_global=True)
138+
schema.register_type(ForgivingTime._default_qname, instance)
139+
134140
document.types.add_documents([None], url)
135141
# Perform the original load
136142
document.original_load(url)

onvif/types.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,90 @@
11
"""ONVIF types."""
22

3+
from datetime import datetime, timedelta, time
34
import ciso8601
4-
from zeep.xsd.types.builtins import DateTime, treat_whitespace
5+
from zeep.xsd.types.builtins import DateTime, treat_whitespace, Time
6+
import isodate
7+
8+
9+
def _try_parse_datetime(value: str) -> datetime | None:
10+
try:
11+
return ciso8601.parse_datetime(value)
12+
except ValueError:
13+
pass
14+
15+
try:
16+
return isodate.parse_datetime(value)
17+
except ValueError:
18+
pass
19+
20+
return None
21+
22+
23+
def _try_fix_time_overflow(time: str) -> tuple[str, dict[str, int]]:
24+
offset: dict[str, int] = {}
25+
hour = int(time[0:2])
26+
if hour > 23:
27+
offset["hours"] = hour - 23
28+
hour = 23
29+
minute = int(time[3:5])
30+
if minute > 59:
31+
offset["minutes"] = minute - 59
32+
minute = 59
33+
second = int(time[6:8])
34+
if second > 59:
35+
offset["seconds"] = second - 59
36+
second = 59
37+
time_trailer = time[8:]
38+
return f"{hour:02d}:{minute:02d}:{second:02d}{time_trailer}", offset
539

640

741
# see https://github.com/mvantellingen/python-zeep/pull/1370
842
class FastDateTime(DateTime):
943
"""Fast DateTime that supports timestamps with - instead of T."""
1044

1145
@treat_whitespace("collapse")
12-
def pythonvalue(self, value):
46+
def pythonvalue(self, value: str) -> datetime:
1347
"""Convert the xml value into a python value."""
1448
if len(value) > 10 and value[10] == "-": # 2010-01-01-00:00:00...
1549
value[10] = "T"
1650
if len(value) > 10 and value[11] == "-": # 2023-05-15T-07:10:32Z...
1751
value = value[:11] + value[12:]
52+
# Determine based on the length of the value if it only contains a date
53+
# lazy hack ;-)
54+
if len(value) == 10:
55+
value += "T00:00:00"
56+
elif (len(value) == 19 or len(value) == 26) and value[10] == " ":
57+
value = "T".join(value.split(" "))
58+
59+
if dt := _try_parse_datetime(value):
60+
return dt
1861

62+
# Some cameras overflow the hours/minutes/seconds
63+
# For example, 2024-08-17T00:61:16Z so we need
64+
# to fix the overflow
65+
date, _, time = value.partition("T")
66+
fixed_time, offset = _try_fix_time_overflow(time)
67+
if dt := _try_parse_datetime(f"{date}T{fixed_time}"):
68+
return dt + timedelta(**offset)
69+
70+
return ciso8601.parse_datetime(value)
71+
72+
73+
class ForgivingTime(Time):
74+
"""ForgivingTime."""
75+
76+
@treat_whitespace("collapse")
77+
def pythonvalue(self, value: str) -> time:
1978
try:
20-
return ciso8601.parse_datetime(value)
79+
return isodate.parse_time(value)
2180
except ValueError:
2281
pass
2382

24-
return super().pythonvalue(value)
83+
# Some cameras overflow the hours/minutes/seconds
84+
# For example, 2024-08-17T00:61:16Z so we need
85+
# to fix the overflow
86+
fixed_time, offset = _try_fix_time_overflow(value)
87+
try:
88+
return isodate.parse_time(fixed_time) + timedelta(**offset)
89+
except ValueError:
90+
return isodate.parse_time(value)

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
[tool.black]
2-
target-version = ["py36", "py37", "py38"]
3-
exclude = 'generated'
1+
[tool.pytest.ini_options]
2+
pythonpath = ["onvif"]
3+
log_cli="true"
4+
log_level="NOTSET"

tests/test_types.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
import pytest
6+
from zeep.loader import parse_xml
7+
import datetime
8+
from onvif.client import ONVIFCamera
9+
from onvif.settings import DEFAULT_SETTINGS
10+
from onvif.transport import ASYNC_TRANSPORT
11+
12+
INVALID_TERM_TIME = b'<?xml version="1.0" encoding="UTF-8"?>\r\n<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:chan="http://schemas.microsoft.com/ws/2005/02/duplex" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tns1="http://www.onvif.org/ver10/topics">\r\n<SOAP-ENV:Header>\r\n<wsa5:Action>http://www.onvif.org/ver10/events/wsdl/PullPointSubscription/PullMessagesResponse</wsa5:Action>\r\n</SOAP-ENV:Header>\r\n<SOAP-ENV:Body>\r\n<tev:PullMessagesResponse>\r\n<tev:CurrentTime>2024-08-17T00:56:16Z</tev:CurrentTime>\r\n<tev:TerminationTime>2024-08-17T00:61:16Z</tev:TerminationTime>\r\n</tev:PullMessagesResponse>\r\n</SOAP-ENV:Body>\r\n</SOAP-ENV:Envelope>\r\n'
13+
_WSDL_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "onvif", "wsdl")
14+
15+
16+
@pytest.mark.asyncio
17+
async def test_parse_invalid_time(caplog: pytest.LogCaptureFixture) -> None:
18+
device = ONVIFCamera("127.0.0.1", 80, "user", "pass", wsdl_dir=_WSDL_PATH)
19+
device.xaddrs = {
20+
"http://www.onvif.org/ver10/events/wsdl": "http://192.168.210.102:6688/onvif/event_service"
21+
}
22+
# Create subscription manager
23+
subscription = await device.create_notification_service()
24+
operation = subscription.document.bindings[subscription.binding_name].get(
25+
"Subscribe"
26+
)
27+
envelope = parse_xml(
28+
INVALID_TERM_TIME, # type: ignore[arg-type]
29+
ASYNC_TRANSPORT,
30+
settings=DEFAULT_SETTINGS,
31+
)
32+
result = operation.process_reply(envelope)
33+
assert result.CurrentTime == datetime.datetime(
34+
2024, 8, 17, 0, 56, 16, tzinfo=datetime.timezone.utc
35+
)
36+
assert result.TerminationTime == datetime.datetime(
37+
2024, 8, 17, 1, 1, 16, tzinfo=datetime.timezone.utc
38+
)
39+
assert "ValueError" not in caplog.text

0 commit comments

Comments
 (0)