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/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: Optional[str] = ARG_DEVICE_ID) -> None:
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: Optional[str] = typer.Argument(None)) -> None:
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))
@@ -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: Optional[str] = ARG_DEVICE_ID) -> None:
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: Optional[str] = ARG_DEVICE_ID) -> None:
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: Optional[str] = typer.Argument(None)) -> None:
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: Optional[str] = ARG_DEVICE_ID) -> None:
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: Optional[str] = typer.Argument(None),
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, TypeVar
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
- extract_type_shape,
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
- type_, shape = extract_type_shape(field.annotation) # type: ignore[arg-type]
224
+ origin, type_ = get_field_type(field.annotation) # type: ignore[arg-type]
228
225
  if _is_protect_base_object(type_):
229
- if shape == SHAPE_LIST_V1:
226
+ if origin is list:
230
227
  lists[name] = type_
231
- elif shape == SHAPE_DICT_V1:
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
- if key in remaps:
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[key]
325
- data[remapped_key] = data.pop(key)
326
- key = remapped_key
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(key)
329
- data[new_key] = data.pop(key)
330
- key = new_key
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 key == "api":
330
+ if current_key == "api":
333
331
  continue
334
332
 
335
- if key not in _fields:
336
- del data[key]
333
+ if current_key not in _fields:
334
+ del data[current_key]
337
335
  continue
338
- data[key] = convert_unifi_data(data[key], _fields[key])
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(cls: ProtectObject, data: dict[str, Any]) -> ProtectObject:
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
- unifi_dicts,
494
- has_unifi_dicts,
495
- ) = cls._get_protect_model()
496
- api = cls._api
497
- _fields = cls.model_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(cls, key)) is not None:
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(cls, key, value)
517
+ setattr(self, key, value)
518
518
 
519
- return cls
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: int | None = None
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: int | None = None
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
@@ -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
- data[key] = {}
223
- _LOGGER.error(
224
- f"Missing key in bootstrap: {key}. This may be fixed by updating Protect."
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
- elif action_type == "remove":
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
- elif action_type == "update":
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
- """Get if current version of Protect is a prerelease version."""
684
- return await self.nvr.get_is_prerelease()
688
+ """[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
689
+ return False