uiprotect 3.8.0__py3-none-any.whl → 7.32.0__py3-none-any.whl
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.
- uiprotect/__init__.py +1 -3
- uiprotect/_compat.py +13 -0
- uiprotect/api.py +975 -92
- uiprotect/cli/__init__.py +111 -24
- uiprotect/cli/aiports.py +58 -0
- uiprotect/cli/backup.py +5 -4
- uiprotect/cli/base.py +5 -5
- uiprotect/cli/cameras.py +152 -13
- uiprotect/cli/chimes.py +5 -6
- uiprotect/cli/doorlocks.py +2 -3
- uiprotect/cli/events.py +7 -8
- uiprotect/cli/lights.py +11 -3
- uiprotect/cli/liveviews.py +1 -2
- uiprotect/cli/sensors.py +2 -3
- uiprotect/cli/viewers.py +2 -3
- uiprotect/data/__init__.py +2 -0
- uiprotect/data/base.py +96 -97
- uiprotect/data/bootstrap.py +116 -45
- uiprotect/data/convert.py +17 -2
- uiprotect/data/devices.py +409 -164
- uiprotect/data/nvr.py +236 -118
- uiprotect/data/types.py +94 -59
- uiprotect/data/user.py +132 -13
- uiprotect/data/websocket.py +2 -1
- uiprotect/stream.py +13 -6
- uiprotect/test_util/__init__.py +47 -7
- uiprotect/test_util/anonymize.py +4 -5
- uiprotect/utils.py +99 -45
- uiprotect/websocket.py +11 -6
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/METADATA +77 -21
- uiprotect-7.32.0.dist-info/RECORD +39 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/WHEEL +1 -1
- uiprotect-3.8.0.dist-info/RECORD +0 -37
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/entry_points.txt +0 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info/licenses}/LICENSE +0 -0
uiprotect/cli/events.py
CHANGED
|
@@ -4,7 +4,6 @@ from collections.abc import Callable
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
from rich.progress import Progress
|
|
@@ -43,13 +42,13 @@ ALL_COMMANDS: dict[str, Callable[..., None]] = {}
|
|
|
43
42
|
@app.callback(invoke_without_command=True)
|
|
44
43
|
def main(
|
|
45
44
|
ctx: typer.Context,
|
|
46
|
-
event_id:
|
|
47
|
-
start:
|
|
48
|
-
end:
|
|
49
|
-
limit:
|
|
50
|
-
offset:
|
|
51
|
-
types:
|
|
52
|
-
smart_types:
|
|
45
|
+
event_id: str | None = ARG_EVENT_ID,
|
|
46
|
+
start: datetime | None = OPTION_START,
|
|
47
|
+
end: datetime | None = OPTION_END,
|
|
48
|
+
limit: int | None = OPTION_LIMIT,
|
|
49
|
+
offset: int | None = OPTION_OFFSET,
|
|
50
|
+
types: list[d.EventType] | None = OPTION_TYPES,
|
|
51
|
+
smart_types: list[d.SmartDetectObjectType] | None = OPTION_SMART_TYPES,
|
|
53
52
|
) -> None:
|
|
54
53
|
"""
|
|
55
54
|
Events CLI.
|
uiprotect/cli/lights.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import timedelta
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
|
|
@@ -25,7 +24,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
@app.callback(invoke_without_command=True)
|
|
28
|
-
def main(ctx: typer.Context, device_id:
|
|
27
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
29
28
|
"""
|
|
30
29
|
Lights device CLI.
|
|
31
30
|
|
|
@@ -59,7 +58,7 @@ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
|
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
@app.command()
|
|
62
|
-
def camera(ctx: typer.Context, camera_id:
|
|
61
|
+
def camera(ctx: typer.Context, camera_id: str | None = typer.Argument(None)) -> None:
|
|
63
62
|
"""Returns or sets tha paired camera for a light."""
|
|
64
63
|
base.require_device_id(ctx)
|
|
65
64
|
obj: Light = ctx.obj.device
|
|
@@ -117,3 +116,12 @@ def set_duration(
|
|
|
117
116
|
obj: Light = ctx.obj.device
|
|
118
117
|
|
|
119
118
|
base.run(ctx, obj.set_duration(timedelta(seconds=duration)))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command()
|
|
122
|
+
def set_flood_light(ctx: typer.Context, enabled: bool) -> None:
|
|
123
|
+
"""Sets flood light (force on) for light device."""
|
|
124
|
+
base.require_device_id(ctx)
|
|
125
|
+
obj: Light = ctx.obj.device
|
|
126
|
+
|
|
127
|
+
base.run(ctx, obj.set_flood_light(enabled))
|
uiprotect/cli/liveviews.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -24,7 +23,7 @@ class LiveviewContext(base.CliContext):
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@app.callback(invoke_without_command=True)
|
|
27
|
-
def main(ctx: typer.Context, device_id:
|
|
26
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
28
27
|
"""
|
|
29
28
|
Liveviews CLI.
|
|
30
29
|
|
uiprotect/cli/sensors.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -24,7 +23,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@app.callback(invoke_without_command=True)
|
|
27
|
-
def main(ctx: typer.Context, device_id:
|
|
26
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
28
27
|
"""
|
|
29
28
|
Sensors device CLI.
|
|
30
29
|
|
|
@@ -58,7 +57,7 @@ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
|
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
@app.command()
|
|
61
|
-
def camera(ctx: typer.Context, camera_id:
|
|
60
|
+
def camera(ctx: typer.Context, camera_id: str | None = typer.Argument(None)) -> None:
|
|
62
61
|
"""Returns or sets tha paired camera for a sensor."""
|
|
63
62
|
base.require_device_id(ctx)
|
|
64
63
|
obj: Sensor = ctx.obj.device
|
uiprotect/cli/viewers.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -24,7 +23,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@app.callback(invoke_without_command=True)
|
|
27
|
-
def main(ctx: typer.Context, device_id:
|
|
26
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
28
27
|
"""
|
|
29
28
|
Viewers device CLI.
|
|
30
29
|
|
|
@@ -60,7 +59,7 @@ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
|
|
|
60
59
|
@app.command()
|
|
61
60
|
def liveview(
|
|
62
61
|
ctx: typer.Context,
|
|
63
|
-
liveview_id:
|
|
62
|
+
liveview_id: str | None = typer.Argument(None),
|
|
64
63
|
) -> None:
|
|
65
64
|
"""Returns or sets the current liveview."""
|
|
66
65
|
base.require_device_id(ctx)
|
uiprotect/data/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from .base import (
|
|
|
10
10
|
from .bootstrap import Bootstrap
|
|
11
11
|
from .convert import create_from_unifi_dict
|
|
12
12
|
from .devices import (
|
|
13
|
+
AiPort,
|
|
13
14
|
Bridge,
|
|
14
15
|
Camera,
|
|
15
16
|
CameraChannel,
|
|
@@ -85,6 +86,7 @@ __all__ = [
|
|
|
85
86
|
"DEFAULT_TYPE",
|
|
86
87
|
"NVR",
|
|
87
88
|
"WS_HEADER_SIZE",
|
|
89
|
+
"AiPort",
|
|
88
90
|
"AnalyticsOption",
|
|
89
91
|
"AudioStyle",
|
|
90
92
|
"Bootstrap",
|
uiprotect/data/base.py
CHANGED
|
@@ -6,14 +6,16 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
|
-
from functools import cache
|
|
10
|
-
from ipaddress import IPv4Address
|
|
11
|
-
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
9
|
+
from functools import cache
|
|
10
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
11
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
12
12
|
from uuid import UUID
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from pydantic
|
|
14
|
+
from convertertools import pop_dict_set_if_none, pop_dict_tuple
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
from pydantic.fields import PrivateAttr
|
|
16
17
|
|
|
18
|
+
from .._compat import cached_property
|
|
17
19
|
from ..exceptions import BadRequest, ClientError, NotAuthorized
|
|
18
20
|
from ..utils import (
|
|
19
21
|
asyncio_timeout,
|
|
@@ -30,6 +32,7 @@ from .types import (
|
|
|
30
32
|
PermissionNode,
|
|
31
33
|
ProtectWSPayloadFormat,
|
|
32
34
|
StateType,
|
|
35
|
+
get_field_type,
|
|
33
36
|
)
|
|
34
37
|
from .websocket import (
|
|
35
38
|
WSJSONPacketFrame,
|
|
@@ -48,7 +51,6 @@ if TYPE_CHECKING:
|
|
|
48
51
|
from ..data.user import User
|
|
49
52
|
|
|
50
53
|
|
|
51
|
-
ProtectObject = TypeVar("ProtectObject", bound="ProtectBaseObject")
|
|
52
54
|
RECENT_EVENT_MAX = timedelta(seconds=30)
|
|
53
55
|
EVENT_PING_INTERVAL = timedelta(seconds=3)
|
|
54
56
|
EVENT_PING_INTERVAL_SECONDS = EVENT_PING_INTERVAL.total_seconds()
|
|
@@ -60,7 +62,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
@cache
|
|
63
|
-
def _is_protect_base_object(cls: type) -> bool:
|
|
65
|
+
def _is_protect_base_object(cls: type[Any]) -> bool:
|
|
64
66
|
"""A cached version of `issubclass(cls, ProtectBaseObject)` to speed up the check."""
|
|
65
67
|
return issubclass(cls, ProtectBaseObject)
|
|
66
68
|
|
|
@@ -91,12 +93,8 @@ class ProtectBaseObject(BaseModel):
|
|
|
91
93
|
* Provides `.unifi_dict` to convert object back into UFP JSON
|
|
92
94
|
"""
|
|
93
95
|
|
|
94
|
-
_api: ProtectApiClient = PrivateAttr(None)
|
|
95
|
-
|
|
96
|
-
class Config:
|
|
97
|
-
arbitrary_types_allowed = True
|
|
98
|
-
validate_assignment = True
|
|
99
|
-
copy_on_model_validation = "shallow"
|
|
96
|
+
_api: ProtectApiClient = PrivateAttr(None) # type: ignore[assignment]
|
|
97
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
|
|
100
98
|
|
|
101
99
|
def __init__(self, api: ProtectApiClient | None = None, **data: Any) -> None:
|
|
102
100
|
"""
|
|
@@ -136,10 +134,12 @@ class ProtectBaseObject(BaseModel):
|
|
|
136
134
|
data.pop("api", None)
|
|
137
135
|
return cls(api=api, **data)
|
|
138
136
|
|
|
139
|
-
return cls.
|
|
137
|
+
return cls.model_construct(**data)
|
|
140
138
|
|
|
141
139
|
@classmethod
|
|
142
|
-
def
|
|
140
|
+
def model_construct(
|
|
141
|
+
cls, _fields_set: set[str] | None = None, **values: Any
|
|
142
|
+
) -> Self:
|
|
143
143
|
api: ProtectApiClient | None = values.pop("api", None)
|
|
144
144
|
(
|
|
145
145
|
unifi_objs,
|
|
@@ -151,19 +151,21 @@ class ProtectBaseObject(BaseModel):
|
|
|
151
151
|
) = cls._get_protect_model()
|
|
152
152
|
for key, value in values.items():
|
|
153
153
|
if has_unifi_objs and key in unifi_objs and isinstance(value, dict):
|
|
154
|
-
values[key] = unifi_objs[key].
|
|
154
|
+
values[key] = unifi_objs[key].model_construct(**value)
|
|
155
155
|
elif has_unifi_lists and key in unifi_lists and isinstance(value, list):
|
|
156
156
|
values[key] = [
|
|
157
|
-
unifi_lists[key].
|
|
157
|
+
unifi_lists[key].model_construct(**v) if isinstance(v, dict) else v
|
|
158
158
|
for v in value
|
|
159
159
|
]
|
|
160
160
|
elif has_unifi_dicts and key in unifi_dicts and isinstance(value, dict):
|
|
161
161
|
values[key] = {
|
|
162
|
-
k: unifi_dicts[key].
|
|
162
|
+
k: unifi_dicts[key].model_construct(**v)
|
|
163
|
+
if isinstance(v, dict)
|
|
164
|
+
else v
|
|
163
165
|
for k, v in value.items()
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
obj = super().
|
|
168
|
+
obj = super().model_construct(_fields_set=_fields_set, **values)
|
|
167
169
|
if api is not None:
|
|
168
170
|
obj._api = api
|
|
169
171
|
|
|
@@ -217,15 +219,16 @@ class ProtectBaseObject(BaseModel):
|
|
|
217
219
|
lists: dict[str, type[ProtectBaseObject]] = {}
|
|
218
220
|
dicts: dict[str, type[ProtectBaseObject]] = {}
|
|
219
221
|
|
|
220
|
-
for name, field in cls.
|
|
222
|
+
for name, field in cls.model_fields.items():
|
|
221
223
|
try:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
origin, type_ = get_field_type(field.annotation) # type: ignore[arg-type]
|
|
225
|
+
if _is_protect_base_object(type_):
|
|
226
|
+
if origin is list:
|
|
227
|
+
lists[name] = type_
|
|
228
|
+
elif origin is dict:
|
|
229
|
+
dicts[name] = type_
|
|
227
230
|
else:
|
|
228
|
-
objs[name] =
|
|
231
|
+
objs[name] = type_
|
|
229
232
|
except TypeError:
|
|
230
233
|
pass
|
|
231
234
|
|
|
@@ -311,25 +314,28 @@ class ProtectBaseObject(BaseModel):
|
|
|
311
314
|
|
|
312
315
|
remaps = cls._get_unifi_remaps()
|
|
313
316
|
# convert to snake_case and remove extra fields
|
|
314
|
-
_fields = cls.
|
|
315
|
-
for key in
|
|
316
|
-
|
|
317
|
+
_fields = cls.model_fields
|
|
318
|
+
for key in data.copy():
|
|
319
|
+
current_key = key
|
|
320
|
+
if current_key in remaps:
|
|
317
321
|
# remap keys that will not be converted correctly by snake_case convert
|
|
318
|
-
remapped_key = remaps[
|
|
319
|
-
data[remapped_key] = data.pop(
|
|
320
|
-
|
|
322
|
+
remapped_key = remaps[current_key]
|
|
323
|
+
data[remapped_key] = data.pop(current_key)
|
|
324
|
+
current_key = remapped_key
|
|
321
325
|
|
|
322
|
-
new_key = to_snake_case(
|
|
323
|
-
data[new_key] = data.pop(
|
|
324
|
-
|
|
326
|
+
new_key = to_snake_case(current_key)
|
|
327
|
+
data[new_key] = data.pop(current_key)
|
|
328
|
+
current_key = new_key
|
|
325
329
|
|
|
326
|
-
if
|
|
330
|
+
if current_key == "api":
|
|
327
331
|
continue
|
|
328
332
|
|
|
329
|
-
if
|
|
330
|
-
del data[
|
|
333
|
+
if current_key not in _fields:
|
|
334
|
+
del data[current_key]
|
|
331
335
|
continue
|
|
332
|
-
data[
|
|
336
|
+
data[current_key] = convert_unifi_data(
|
|
337
|
+
data[current_key], _fields[current_key]
|
|
338
|
+
)
|
|
333
339
|
|
|
334
340
|
if not data:
|
|
335
341
|
return data
|
|
@@ -367,7 +373,7 @@ class ProtectBaseObject(BaseModel):
|
|
|
367
373
|
if isinstance(value, ProtectBaseObject):
|
|
368
374
|
value = value.unifi_dict()
|
|
369
375
|
elif isinstance(value, dict):
|
|
370
|
-
value = klass.
|
|
376
|
+
value = klass.model_construct({}).unifi_dict(data=value) # type: ignore[arg-type]
|
|
371
377
|
|
|
372
378
|
return value
|
|
373
379
|
|
|
@@ -388,7 +394,7 @@ class ProtectBaseObject(BaseModel):
|
|
|
388
394
|
return [
|
|
389
395
|
item.unifi_dict()
|
|
390
396
|
if isinstance(item, ProtectBaseObject)
|
|
391
|
-
else klass.
|
|
397
|
+
else klass.model_construct({}).unifi_dict(data=item) # type: ignore[arg-type]
|
|
392
398
|
for item in value
|
|
393
399
|
]
|
|
394
400
|
|
|
@@ -435,7 +441,7 @@ class ProtectBaseObject(BaseModel):
|
|
|
435
441
|
excluded_fields = self._get_excluded_fields()
|
|
436
442
|
if exclude is not None:
|
|
437
443
|
excluded_fields = excluded_fields.copy() | exclude
|
|
438
|
-
data = self.
|
|
444
|
+
data = self.model_dump(exclude=excluded_fields)
|
|
439
445
|
use_obj = True
|
|
440
446
|
|
|
441
447
|
(
|
|
@@ -472,7 +478,7 @@ class ProtectBaseObject(BaseModel):
|
|
|
472
478
|
|
|
473
479
|
return new_data
|
|
474
480
|
|
|
475
|
-
def update_from_dict(
|
|
481
|
+
def update_from_dict(self, data: dict[str, Any]) -> Self:
|
|
476
482
|
"""
|
|
477
483
|
Updates current object from a cleaned UFP JSON dict.
|
|
478
484
|
|
|
@@ -484,17 +490,17 @@ class ProtectBaseObject(BaseModel):
|
|
|
484
490
|
has_unifi_objs,
|
|
485
491
|
unifi_lists,
|
|
486
492
|
has_unifi_lists,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
) =
|
|
490
|
-
api =
|
|
491
|
-
_fields =
|
|
493
|
+
_unifi_dicts,
|
|
494
|
+
_has_unifi_dicts,
|
|
495
|
+
) = self._get_protect_model()
|
|
496
|
+
api = self._api
|
|
497
|
+
_fields = self.__class__.model_fields
|
|
492
498
|
unifi_obj: ProtectBaseObject | None
|
|
493
499
|
value: Any
|
|
494
500
|
|
|
495
501
|
for key, item in data.items():
|
|
496
502
|
if has_unifi_objs and key in unifi_objs and isinstance(item, dict):
|
|
497
|
-
if (unifi_obj := getattr(
|
|
503
|
+
if (unifi_obj := getattr(self, key)) is not None:
|
|
498
504
|
value = unifi_obj.update_from_dict(item)
|
|
499
505
|
else:
|
|
500
506
|
value = unifi_objs[key](**item, api=api)
|
|
@@ -508,17 +514,17 @@ class ProtectBaseObject(BaseModel):
|
|
|
508
514
|
else:
|
|
509
515
|
value = convert_unifi_data(item, _fields[key])
|
|
510
516
|
|
|
511
|
-
setattr(
|
|
517
|
+
setattr(self, key, value)
|
|
512
518
|
|
|
513
|
-
return
|
|
519
|
+
return self
|
|
514
520
|
|
|
515
521
|
def dict_with_excludes(self) -> dict[str, Any]:
|
|
516
522
|
"""Returns a dict of the current object without any UFP objects converted to dicts."""
|
|
517
523
|
excludes = self.__class__._get_excluded_changed_fields()
|
|
518
|
-
return self.
|
|
524
|
+
return self.model_dump(exclude=excludes)
|
|
519
525
|
|
|
520
526
|
def get_changed(self, data_before_changes: dict[str, Any]) -> dict[str, Any]:
|
|
521
|
-
return dict_diff(data_before_changes, self.
|
|
527
|
+
return dict_diff(data_before_changes, self.model_dump())
|
|
522
528
|
|
|
523
529
|
@property
|
|
524
530
|
def api(self) -> ProtectApiClient:
|
|
@@ -538,7 +544,7 @@ class ProtectModel(ProtectBaseObject):
|
|
|
538
544
|
automatically decoding a `modelKey` object into the correct UFP object and type
|
|
539
545
|
"""
|
|
540
546
|
|
|
541
|
-
model: ModelType | None
|
|
547
|
+
model: ModelType | None = None
|
|
542
548
|
|
|
543
549
|
@classmethod
|
|
544
550
|
@cache
|
|
@@ -551,10 +557,7 @@ class ProtectModel(ProtectBaseObject):
|
|
|
551
557
|
exclude: set[str] | None = None,
|
|
552
558
|
) -> dict[str, Any]:
|
|
553
559
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
554
|
-
|
|
555
|
-
if "modelKey" in data and data["modelKey"] is None:
|
|
556
|
-
del data["modelKey"]
|
|
557
|
-
|
|
560
|
+
pop_dict_set_if_none(data, {"modelKey"})
|
|
558
561
|
return data
|
|
559
562
|
|
|
560
563
|
|
|
@@ -580,7 +583,7 @@ class UpdateSynchronization:
|
|
|
580
583
|
class ProtectModelWithId(ProtectModel):
|
|
581
584
|
id: str
|
|
582
585
|
|
|
583
|
-
_update_sync: UpdateSynchronization = PrivateAttr(None)
|
|
586
|
+
_update_sync: UpdateSynchronization = PrivateAttr(None) # type: ignore[assignment]
|
|
584
587
|
|
|
585
588
|
def __init__(self, **data: Any) -> None:
|
|
586
589
|
update_sync = data.pop("update_sync", None)
|
|
@@ -588,9 +591,11 @@ class ProtectModelWithId(ProtectModel):
|
|
|
588
591
|
self._update_sync = update_sync or UpdateSynchronization()
|
|
589
592
|
|
|
590
593
|
@classmethod
|
|
591
|
-
def
|
|
594
|
+
def model_construct(
|
|
595
|
+
cls, _fields_set: set[str] | None = None, **values: Any
|
|
596
|
+
) -> Self:
|
|
592
597
|
update_sync = values.pop("update_sync", None)
|
|
593
|
-
obj = super().
|
|
598
|
+
obj = super().model_construct(_fields_set=_fields_set, **values)
|
|
594
599
|
obj._update_sync = update_sync or UpdateSynchronization()
|
|
595
600
|
return obj
|
|
596
601
|
|
|
@@ -716,9 +721,9 @@ class ProtectModelWithId(ProtectModel):
|
|
|
716
721
|
updated,
|
|
717
722
|
)
|
|
718
723
|
|
|
719
|
-
assert (
|
|
720
|
-
|
|
721
|
-
)
|
|
724
|
+
assert self._update_sync.lock.locked(), (
|
|
725
|
+
"save_device_changes should only be called when the update lock is held"
|
|
726
|
+
)
|
|
722
727
|
read_only_fields = self.__class__._get_read_only_fields()
|
|
723
728
|
|
|
724
729
|
if self.model is None:
|
|
@@ -795,15 +800,15 @@ class ProtectModelWithId(ProtectModel):
|
|
|
795
800
|
|
|
796
801
|
|
|
797
802
|
class ProtectDeviceModel(ProtectModelWithId):
|
|
798
|
-
name: str | None
|
|
803
|
+
name: str | None = None
|
|
799
804
|
type: str
|
|
800
805
|
mac: str
|
|
801
|
-
host: IPv4Address | str | None
|
|
802
|
-
up_since: datetime | None
|
|
803
|
-
uptime: timedelta | None
|
|
804
|
-
last_seen: datetime | None
|
|
805
|
-
hardware_revision: str | None
|
|
806
|
-
firmware_version: str | None
|
|
806
|
+
host: IPv4Address | str | None = None
|
|
807
|
+
up_since: datetime | None = None
|
|
808
|
+
uptime: timedelta | None = None
|
|
809
|
+
last_seen: datetime | None = None
|
|
810
|
+
hardware_revision: str | None = None
|
|
811
|
+
firmware_version: str | None = None
|
|
807
812
|
is_updating: bool
|
|
808
813
|
is_ssh_enabled: bool
|
|
809
814
|
|
|
@@ -854,12 +859,12 @@ class ProtectDeviceModel(ProtectModelWithId):
|
|
|
854
859
|
|
|
855
860
|
|
|
856
861
|
class WiredConnectionState(ProtectBaseObject):
|
|
857
|
-
phy_rate: float | None
|
|
862
|
+
phy_rate: float | None = None
|
|
858
863
|
|
|
859
864
|
|
|
860
865
|
class WirelessConnectionState(ProtectBaseObject):
|
|
861
|
-
signal_quality: int | None
|
|
862
|
-
signal_strength: int | None
|
|
866
|
+
signal_quality: int | None = None
|
|
867
|
+
signal_strength: int | None = None
|
|
863
868
|
|
|
864
869
|
|
|
865
870
|
class BluetoothConnectionState(WirelessConnectionState):
|
|
@@ -867,10 +872,10 @@ class BluetoothConnectionState(WirelessConnectionState):
|
|
|
867
872
|
|
|
868
873
|
|
|
869
874
|
class WifiConnectionState(WirelessConnectionState):
|
|
870
|
-
phy_rate: float | None
|
|
871
|
-
channel: int | None
|
|
872
|
-
frequency: int | None
|
|
873
|
-
ssid: str | None
|
|
875
|
+
phy_rate: float | None = None
|
|
876
|
+
channel: int | None = None
|
|
877
|
+
frequency: int | None = None
|
|
878
|
+
ssid: str | None = None
|
|
874
879
|
bssid: str | None = None
|
|
875
880
|
tx_rate: float | None = None
|
|
876
881
|
# requires 2.7.5+
|
|
@@ -882,10 +887,10 @@ class WifiConnectionState(WirelessConnectionState):
|
|
|
882
887
|
|
|
883
888
|
class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
884
889
|
state: StateType
|
|
885
|
-
connection_host: IPv4Address | str | None
|
|
886
|
-
connected_since: datetime | None
|
|
887
|
-
latest_firmware_version: str | None
|
|
888
|
-
firmware_build: str | None
|
|
890
|
+
connection_host: IPv4Address | IPv6Address | str | None = None
|
|
891
|
+
connected_since: datetime | None = None
|
|
892
|
+
latest_firmware_version: str | None = None
|
|
893
|
+
firmware_build: str | None = None
|
|
889
894
|
is_adopting: bool
|
|
890
895
|
is_adopted: bool
|
|
891
896
|
is_adopted_by_other: bool
|
|
@@ -895,7 +900,7 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
|
895
900
|
is_attempting_to_connect: bool
|
|
896
901
|
is_connected: bool
|
|
897
902
|
# requires 1.21+
|
|
898
|
-
market_name: str | None
|
|
903
|
+
market_name: str | None = None
|
|
899
904
|
# requires 2.7.5+
|
|
900
905
|
fw_update_state: str | None = None
|
|
901
906
|
# requires 2.8.14+
|
|
@@ -910,8 +915,8 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
|
910
915
|
wired_connection_state: WiredConnectionState | None = None
|
|
911
916
|
wifi_connection_state: WifiConnectionState | None = None
|
|
912
917
|
bluetooth_connection_state: BluetoothConnectionState | None = None
|
|
913
|
-
bridge_id: str | None
|
|
914
|
-
is_downloading_firmware: bool | None
|
|
918
|
+
bridge_id: str | None = None
|
|
919
|
+
is_downloading_firmware: bool | None = None
|
|
915
920
|
|
|
916
921
|
# TODO:
|
|
917
922
|
# bridgeCandidates
|
|
@@ -955,13 +960,10 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
|
955
960
|
exclude: set[str] | None = None,
|
|
956
961
|
) -> dict[str, Any]:
|
|
957
962
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
"wifiConnectionState",
|
|
961
|
-
|
|
962
|
-
):
|
|
963
|
-
if key in data and data[key] is None:
|
|
964
|
-
del data[key]
|
|
963
|
+
pop_dict_set_if_none(
|
|
964
|
+
data,
|
|
965
|
+
{"wiredConnectionState", "wifiConnectionState", "bluetoothConnectionState"},
|
|
966
|
+
)
|
|
965
967
|
return data
|
|
966
968
|
|
|
967
969
|
@classmethod
|
|
@@ -1055,7 +1057,7 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
|
1055
1057
|
|
|
1056
1058
|
|
|
1057
1059
|
class ProtectMotionDeviceModel(ProtectAdoptableDeviceModel):
|
|
1058
|
-
last_motion: datetime | None
|
|
1060
|
+
last_motion: datetime | None = None
|
|
1059
1061
|
is_dark: bool
|
|
1060
1062
|
|
|
1061
1063
|
# not directly from UniFi
|
|
@@ -1072,10 +1074,7 @@ class ProtectMotionDeviceModel(ProtectAdoptableDeviceModel):
|
|
|
1072
1074
|
exclude: set[str] | None = None,
|
|
1073
1075
|
) -> dict[str, Any]:
|
|
1074
1076
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
1075
|
-
|
|
1076
|
-
if "lastMotionEventId" in data:
|
|
1077
|
-
del data["lastMotionEventId"]
|
|
1078
|
-
|
|
1077
|
+
pop_dict_tuple(data, ("lastMotionEventId",))
|
|
1079
1078
|
return data
|
|
1080
1079
|
|
|
1081
1080
|
@property
|