uiprotect 7.5.2__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/api.py +886 -59
- uiprotect/cli/__init__.py +109 -24
- uiprotect/cli/aiports.py +1 -2
- uiprotect/cli/backup.py +5 -4
- uiprotect/cli/base.py +4 -4
- 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/base.py +32 -32
- uiprotect/data/bootstrap.py +20 -15
- uiprotect/data/devices.py +183 -16
- uiprotect/data/nvr.py +139 -38
- uiprotect/data/types.py +32 -19
- uiprotect/stream.py +13 -2
- uiprotect/test_util/__init__.py +30 -7
- uiprotect/test_util/anonymize.py +4 -5
- uiprotect/utils.py +56 -24
- uiprotect/websocket.py +3 -3
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/METADATA +70 -17
- uiprotect-7.32.0.dist-info/RECORD +39 -0
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/WHEEL +1 -1
- uiprotect-7.5.2.dist-info/RECORD +0 -39
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/entry_points.txt +0 -0
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info/licenses}/LICENSE +0 -0
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/base.py
CHANGED
|
@@ -7,8 +7,8 @@ import logging
|
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
from functools import cache
|
|
10
|
-
from ipaddress import IPv4Address
|
|
11
|
-
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
10
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
11
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
12
12
|
from uuid import UUID
|
|
13
13
|
|
|
14
14
|
from convertertools import pop_dict_set_if_none, pop_dict_tuple
|
|
@@ -27,14 +27,12 @@ from ..utils import (
|
|
|
27
27
|
to_snake_case,
|
|
28
28
|
)
|
|
29
29
|
from .types import (
|
|
30
|
-
SHAPE_DICT_V1,
|
|
31
|
-
SHAPE_LIST_V1,
|
|
32
30
|
ModelType,
|
|
33
31
|
PercentFloat,
|
|
34
32
|
PermissionNode,
|
|
35
33
|
ProtectWSPayloadFormat,
|
|
36
34
|
StateType,
|
|
37
|
-
|
|
35
|
+
get_field_type,
|
|
38
36
|
)
|
|
39
37
|
from .websocket import (
|
|
40
38
|
WSJSONPacketFrame,
|
|
@@ -53,7 +51,6 @@ if TYPE_CHECKING:
|
|
|
53
51
|
from ..data.user import User
|
|
54
52
|
|
|
55
53
|
|
|
56
|
-
ProtectObject = TypeVar("ProtectObject", bound="ProtectBaseObject")
|
|
57
54
|
RECENT_EVENT_MAX = timedelta(seconds=30)
|
|
58
55
|
EVENT_PING_INTERVAL = timedelta(seconds=3)
|
|
59
56
|
EVENT_PING_INTERVAL_SECONDS = EVENT_PING_INTERVAL.total_seconds()
|
|
@@ -224,11 +221,11 @@ class ProtectBaseObject(BaseModel):
|
|
|
224
221
|
|
|
225
222
|
for name, field in cls.model_fields.items():
|
|
226
223
|
try:
|
|
227
|
-
|
|
224
|
+
origin, type_ = get_field_type(field.annotation) # type: ignore[arg-type]
|
|
228
225
|
if _is_protect_base_object(type_):
|
|
229
|
-
if
|
|
226
|
+
if origin is list:
|
|
230
227
|
lists[name] = type_
|
|
231
|
-
elif
|
|
228
|
+
elif origin is dict:
|
|
232
229
|
dicts[name] = type_
|
|
233
230
|
else:
|
|
234
231
|
objs[name] = type_
|
|
@@ -319,23 +316,26 @@ class ProtectBaseObject(BaseModel):
|
|
|
319
316
|
# convert to snake_case and remove extra fields
|
|
320
317
|
_fields = cls.model_fields
|
|
321
318
|
for key in data.copy():
|
|
322
|
-
|
|
319
|
+
current_key = key
|
|
320
|
+
if current_key in remaps:
|
|
323
321
|
# remap keys that will not be converted correctly by snake_case convert
|
|
324
|
-
remapped_key = remaps[
|
|
325
|
-
data[remapped_key] = data.pop(
|
|
326
|
-
|
|
322
|
+
remapped_key = remaps[current_key]
|
|
323
|
+
data[remapped_key] = data.pop(current_key)
|
|
324
|
+
current_key = remapped_key
|
|
327
325
|
|
|
328
|
-
new_key = to_snake_case(
|
|
329
|
-
data[new_key] = data.pop(
|
|
330
|
-
|
|
326
|
+
new_key = to_snake_case(current_key)
|
|
327
|
+
data[new_key] = data.pop(current_key)
|
|
328
|
+
current_key = new_key
|
|
331
329
|
|
|
332
|
-
if
|
|
330
|
+
if current_key == "api":
|
|
333
331
|
continue
|
|
334
332
|
|
|
335
|
-
if
|
|
336
|
-
del data[
|
|
333
|
+
if current_key not in _fields:
|
|
334
|
+
del data[current_key]
|
|
337
335
|
continue
|
|
338
|
-
data[
|
|
336
|
+
data[current_key] = convert_unifi_data(
|
|
337
|
+
data[current_key], _fields[current_key]
|
|
338
|
+
)
|
|
339
339
|
|
|
340
340
|
if not data:
|
|
341
341
|
return data
|
|
@@ -478,7 +478,7 @@ class ProtectBaseObject(BaseModel):
|
|
|
478
478
|
|
|
479
479
|
return new_data
|
|
480
480
|
|
|
481
|
-
def update_from_dict(
|
|
481
|
+
def update_from_dict(self, data: dict[str, Any]) -> Self:
|
|
482
482
|
"""
|
|
483
483
|
Updates current object from a cleaned UFP JSON dict.
|
|
484
484
|
|
|
@@ -490,17 +490,17 @@ class ProtectBaseObject(BaseModel):
|
|
|
490
490
|
has_unifi_objs,
|
|
491
491
|
unifi_lists,
|
|
492
492
|
has_unifi_lists,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
) =
|
|
496
|
-
api =
|
|
497
|
-
_fields =
|
|
493
|
+
_unifi_dicts,
|
|
494
|
+
_has_unifi_dicts,
|
|
495
|
+
) = self._get_protect_model()
|
|
496
|
+
api = self._api
|
|
497
|
+
_fields = self.__class__.model_fields
|
|
498
498
|
unifi_obj: ProtectBaseObject | None
|
|
499
499
|
value: Any
|
|
500
500
|
|
|
501
501
|
for key, item in data.items():
|
|
502
502
|
if has_unifi_objs and key in unifi_objs and isinstance(item, dict):
|
|
503
|
-
if (unifi_obj := getattr(
|
|
503
|
+
if (unifi_obj := getattr(self, key)) is not None:
|
|
504
504
|
value = unifi_obj.update_from_dict(item)
|
|
505
505
|
else:
|
|
506
506
|
value = unifi_objs[key](**item, api=api)
|
|
@@ -514,9 +514,9 @@ class ProtectBaseObject(BaseModel):
|
|
|
514
514
|
else:
|
|
515
515
|
value = convert_unifi_data(item, _fields[key])
|
|
516
516
|
|
|
517
|
-
setattr(
|
|
517
|
+
setattr(self, key, value)
|
|
518
518
|
|
|
519
|
-
return
|
|
519
|
+
return self
|
|
520
520
|
|
|
521
521
|
def dict_with_excludes(self) -> dict[str, Any]:
|
|
522
522
|
"""Returns a dict of the current object without any UFP objects converted to dicts."""
|
|
@@ -859,7 +859,7 @@ class ProtectDeviceModel(ProtectModelWithId):
|
|
|
859
859
|
|
|
860
860
|
|
|
861
861
|
class WiredConnectionState(ProtectBaseObject):
|
|
862
|
-
phy_rate:
|
|
862
|
+
phy_rate: float | None = None
|
|
863
863
|
|
|
864
864
|
|
|
865
865
|
class WirelessConnectionState(ProtectBaseObject):
|
|
@@ -872,7 +872,7 @@ class BluetoothConnectionState(WirelessConnectionState):
|
|
|
872
872
|
|
|
873
873
|
|
|
874
874
|
class WifiConnectionState(WirelessConnectionState):
|
|
875
|
-
phy_rate:
|
|
875
|
+
phy_rate: float | None = None
|
|
876
876
|
channel: int | None = None
|
|
877
877
|
frequency: int | None = None
|
|
878
878
|
ssid: str | None = None
|
|
@@ -887,7 +887,7 @@ class WifiConnectionState(WirelessConnectionState):
|
|
|
887
887
|
|
|
888
888
|
class ProtectAdoptableDeviceModel(ProtectDeviceModel):
|
|
889
889
|
state: StateType
|
|
890
|
-
connection_host: IPv4Address | str | None = None
|
|
890
|
+
connection_host: IPv4Address | IPv6Address | str | None = None
|
|
891
891
|
connected_since: datetime | None = None
|
|
892
892
|
latest_firmware_version: str | None = None
|
|
893
893
|
firmware_build: str | None = None
|
uiprotect/data/bootstrap.py
CHANGED
|
@@ -178,12 +178,12 @@ class Bootstrap(ProtectBaseObject):
|
|
|
178
178
|
liveviews: dict[str, Liveview]
|
|
179
179
|
nvr: NVR
|
|
180
180
|
viewers: dict[str, Viewer]
|
|
181
|
-
lights: dict[str, Light]
|
|
182
|
-
bridges: dict[str, Bridge]
|
|
183
|
-
sensors: dict[str, Sensor]
|
|
184
|
-
doorlocks: dict[str, Doorlock]
|
|
185
|
-
chimes: dict[str, Chime]
|
|
186
|
-
aiports: dict[str, AiPort]
|
|
181
|
+
lights: dict[str, Light] = {}
|
|
182
|
+
bridges: dict[str, Bridge] = {}
|
|
183
|
+
sensors: dict[str, Sensor] = {}
|
|
184
|
+
doorlocks: dict[str, Doorlock] = {}
|
|
185
|
+
chimes: dict[str, Chime] = {}
|
|
186
|
+
aiports: dict[str, AiPort] = {}
|
|
187
187
|
ringtones: list[Ringtone]
|
|
188
188
|
last_update_id: str
|
|
189
189
|
|
|
@@ -215,14 +215,19 @@ class Bootstrap(ProtectBaseObject):
|
|
|
215
215
|
data["idLookup"] = id_lookup
|
|
216
216
|
data["macLookup"] = mac_lookup
|
|
217
217
|
|
|
218
|
+
# Fields that are not (always?) available in newer Protect versions
|
|
219
|
+
optional_fields = {"doorlocks"}
|
|
220
|
+
|
|
218
221
|
for model_type in ModelType.bootstrap_models_types_set:
|
|
219
222
|
key = model_type.devices_key # type: ignore[attr-defined]
|
|
220
223
|
items: dict[str, ProtectModel] = {}
|
|
221
224
|
if key not in data:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
# Optional fields with defaults don't need logging or setting
|
|
226
|
+
if key not in optional_fields:
|
|
227
|
+
data[key] = {}
|
|
228
|
+
_LOGGER.error(
|
|
229
|
+
f"Missing key in bootstrap: {key}. This may be fixed by updating Protect."
|
|
230
|
+
)
|
|
226
231
|
continue
|
|
227
232
|
for item in data[key]:
|
|
228
233
|
if (
|
|
@@ -420,7 +425,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
420
425
|
changed_data=add_obj.model_dump(),
|
|
421
426
|
new_obj=add_obj,
|
|
422
427
|
)
|
|
423
|
-
|
|
428
|
+
if action_type == "remove":
|
|
424
429
|
to_remove = obj_from_bootstrap.by_id(action_id)
|
|
425
430
|
if to_remove is None:
|
|
426
431
|
return None
|
|
@@ -431,7 +436,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
431
436
|
changed_data={},
|
|
432
437
|
old_obj=to_remove,
|
|
433
438
|
)
|
|
434
|
-
|
|
439
|
+
if action_type == "update":
|
|
435
440
|
updated_obj = obj_from_bootstrap.by_id(action_id)
|
|
436
441
|
if updated_obj is None:
|
|
437
442
|
return None
|
|
@@ -588,7 +593,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
588
593
|
|
|
589
594
|
return message
|
|
590
595
|
|
|
591
|
-
def _make_ws_packet_message(
|
|
596
|
+
def _make_ws_packet_message( # noqa: PLR0911
|
|
592
597
|
self,
|
|
593
598
|
action: dict[str, Any],
|
|
594
599
|
data: dict[str, Any],
|
|
@@ -680,5 +685,5 @@ class Bootstrap(ProtectBaseObject):
|
|
|
680
685
|
_LOGGER.debug("Successfully refresh model: %s %s", model_type, device_id)
|
|
681
686
|
|
|
682
687
|
async def get_is_prerelease(self) -> bool:
|
|
683
|
-
"""
|
|
684
|
-
return
|
|
688
|
+
"""[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
|
|
689
|
+
return False
|