|
1 | 1 | """ONVIF types."""
|
2 | 2 |
|
| 3 | +from datetime import datetime, timedelta, time |
3 | 4 | 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 |
5 | 39 |
|
6 | 40 |
|
7 | 41 | # see https://github.com/mvantellingen/python-zeep/pull/1370
|
8 | 42 | class FastDateTime(DateTime):
|
9 | 43 | """Fast DateTime that supports timestamps with - instead of T."""
|
10 | 44 |
|
11 | 45 | @treat_whitespace("collapse")
|
12 |
| - def pythonvalue(self, value): |
| 46 | + def pythonvalue(self, value: str) -> datetime: |
13 | 47 | """Convert the xml value into a python value."""
|
14 | 48 | if len(value) > 10 and value[10] == "-": # 2010-01-01-00:00:00...
|
15 | 49 | value[10] = "T"
|
16 | 50 | if len(value) > 10 and value[11] == "-": # 2023-05-15T-07:10:32Z...
|
17 | 51 | 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 |
18 | 61 |
|
| 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: |
19 | 78 | try:
|
20 |
| - return ciso8601.parse_datetime(value) |
| 79 | + return isodate.parse_time(value) |
21 | 80 | except ValueError:
|
22 | 81 | pass
|
23 | 82 |
|
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) |
0 commit comments