camera-ui-sdk 1.4.2__tar.gz → 1.4.4__tar.gz

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.
Files changed (87) hide show
  1. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/PKG-INFO +4 -4
  2. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/device.py +16 -1
  3. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/__init__.py +1 -9
  4. camera_ui_sdk-1.4.4/camera_ui_sdk/internal/camera_config_internal.py +29 -0
  5. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/camera_enums.py +0 -2
  6. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/camera_wire.py +0 -4
  7. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/event_emitter.py +0 -24
  8. camera_ui_sdk-1.4.4/camera_ui_sdk/internal/manager_rpc.py +15 -0
  9. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/sensor_rpc.py +2 -31
  10. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/shared_utils.py +0 -15
  11. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/internal/streaming_internal.py +0 -5
  12. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/manager/__init__.py +2 -0
  13. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/observable/__init__.py +0 -1
  14. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/base.py +1 -14
  15. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/classifier.py +1 -2
  16. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/face.py +1 -2
  17. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/license_plate.py +1 -2
  18. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk.egg-info/PKG-INFO +4 -4
  19. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk.egg-info/requires.txt +3 -3
  20. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/pyproject.toml +5 -5
  21. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/tests/test_event_emitter.py +2 -26
  22. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/tests/test_observable.py +0 -25
  23. camera_ui_sdk-1.4.2/camera_ui_sdk/internal/camera_config_internal.py +0 -50
  24. camera_ui_sdk-1.4.2/camera_ui_sdk/internal/manager_rpc.py +0 -24
  25. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/LICENSE.md +0 -0
  26. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/README.md +0 -0
  27. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/__init__.py +0 -0
  28. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/__init__.py +0 -0
  29. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/config.py +0 -0
  30. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/detection.py +0 -0
  31. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/enums.py +0 -0
  32. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/events.py +0 -0
  33. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/frames.py +0 -0
  34. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/notification.py +0 -0
  35. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/recording.py +0 -0
  36. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/camera/streaming.py +0 -0
  37. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/__init__.py +0 -0
  38. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/api.py +0 -0
  39. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/assistant.py +0 -0
  40. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/assistant_models.py +0 -0
  41. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/contract.py +0 -0
  42. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/helper.py +0 -0
  43. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/interfaces.py +0 -0
  44. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/notifier.py +0 -0
  45. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/plugin/oauth.py +0 -0
  46. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/py.typed +0 -0
  47. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/__init__.py +0 -0
  48. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/audio.py +0 -0
  49. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/battery.py +0 -0
  50. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/carbon_dioxide.py +0 -0
  51. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/carbon_monoxide.py +0 -0
  52. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/clip.py +0 -0
  53. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/cold.py +0 -0
  54. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/contact.py +0 -0
  55. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/detection.py +0 -0
  56. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/doorbell.py +0 -0
  57. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/face_embedder.py +0 -0
  58. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/garage.py +0 -0
  59. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/gas.py +0 -0
  60. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/heat.py +0 -0
  61. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/humidity.py +0 -0
  62. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/illuminance.py +0 -0
  63. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/leak.py +0 -0
  64. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/light.py +0 -0
  65. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/lock.py +0 -0
  66. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/motion.py +0 -0
  67. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/object.py +0 -0
  68. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/occupancy.py +0 -0
  69. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/person_embedder.py +0 -0
  70. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/power.py +0 -0
  71. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/problem.py +0 -0
  72. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/ptz.py +0 -0
  73. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/security_system.py +0 -0
  74. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/segmenter.py +0 -0
  75. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/siren.py +0 -0
  76. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/smoke.py +0 -0
  77. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/spec.py +0 -0
  78. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/switch.py +0 -0
  79. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/tamper.py +0 -0
  80. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/temperature.py +0 -0
  81. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/sensor/vibration.py +0 -0
  82. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/storage/__init__.py +0 -0
  83. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk/types.py +0 -0
  84. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk.egg-info/SOURCES.txt +0 -0
  85. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk.egg-info/dependency_links.txt +0 -0
  86. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/camera_ui_sdk.egg-info/top_level.txt +0 -0
  87. {camera_ui_sdk-1.4.2 → camera_ui_sdk-1.4.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camera-ui-sdk
3
- Version: 1.4.2
3
+ Version: 1.4.4
4
4
  Summary: camera.ui sdk
5
5
  Author-email: seydx <hi@seydx.dev>
6
6
  Maintainer-email: seydx <hi@seydx.dev>
@@ -28,10 +28,10 @@ Provides-Extra: dev
28
28
  Requires-Dist: build>=1.6.1; extra == "dev"
29
29
  Requires-Dist: twine>=7.0.0; extra == "dev"
30
30
  Requires-Dist: mypy>=2.4.0; extra == "dev"
31
- Requires-Dist: ruff>=0.17.1; extra == "dev"
31
+ Requires-Dist: ruff>=0.17.2; extra == "dev"
32
32
  Provides-Extra: docs
33
- Requires-Dist: mkdocs-material>=9.7.7; extra == "docs"
34
- Requires-Dist: mkdocstrings[python]>=1.0.7; extra == "docs"
33
+ Requires-Dist: mkdocs-material>=9.8.0; extra == "docs"
34
+ Requires-Dist: mkdocstrings[python]>=1.0.8; extra == "docs"
35
35
  Requires-Dist: mkdocs>=1.6.1; extra == "docs"
36
36
  Requires-Dist: mkdocs-exclude>=1.0.2; extra == "docs"
37
37
  Dynamic: license-file
@@ -245,7 +245,7 @@ class CameraDevice(Protocol):
245
245
 
246
246
  @property
247
247
  def snapshotSource(self) -> CameraSource | None:
248
- """Snapshot source (if available)."""
248
+ """Source that getSnapshot uses: the snapshot source or the one marked for snapshots, otherwise the low, mid or high resolution source."""
249
249
  ...
250
250
 
251
251
  @property
@@ -296,6 +296,21 @@ class CameraDevice(Protocol):
296
296
  """
297
297
  ...
298
298
 
299
+ async def getSnapshot(self, forceNew: bool = False) -> bytes | None:
300
+ """
301
+ Get a snapshot from the snapshot source, see ``snapshotSource``.
302
+
303
+ Args:
304
+ forceNew: Force fresh snapshot (ignore cache).
305
+
306
+ Returns:
307
+ JPEG image data, or None if the camera has no source or no picture.
308
+
309
+ Example:
310
+ jpeg = await camera.getSnapshot()
311
+ """
312
+ ...
313
+
299
314
  def createStorage(self, schemas: list[JsonSchema]) -> DeviceStorage:
300
315
  """
301
316
  Create storage for plugin-specific camera configuration.
@@ -20,33 +20,25 @@ if TYPE_CHECKING:
20
20
  from .streaming_internal import IceServer
21
21
 
22
22
  __all__ = [
23
- # Events
24
23
  "AsyncEventEmitter",
25
- # Manager
26
24
  "ConnectionStatus",
27
25
  "DiscoveredCameraWithState",
28
- # Sensor wire-format / RPC
29
26
  "SensorJSON",
30
27
  "PropertyChangedEvent",
31
28
  "PropertyUpdateFn",
32
29
  "CapabilityUpdateFn",
33
30
  "SensorSourcePatch",
34
31
  "SourceUpdateFn",
35
- # Camera wire-format
36
32
  "DetectionEventMessage",
37
- # Streaming
38
33
  "IceServer",
39
- # Camera enums
40
34
  "FrameType",
41
35
  "CameraFrameWorkerDecoder",
42
- # Camera config
43
36
  "CameraInputSettings",
44
37
  "CameraConfigPartial",
45
- # Utils
46
38
  "is_equal",
47
39
  ]
48
40
 
49
- # public name → (submodule, attribute), resolved lazily to avoid circular imports during SDK bootstrap
41
+ # resolved lazily to avoid circular imports during SDK bootstrap
50
42
  _LAZY: dict[str, tuple[str, str]] = {
51
43
  "AsyncEventEmitter": (".event_emitter", "AsyncEventEmitter"),
52
44
  "ConnectionStatus": (".manager_rpc", "ConnectionStatus"),
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NotRequired, TypedDict
4
+
5
+ from ..camera.config import CameraConfigInputSettings, CameraInformation
6
+ from ..camera.enums import CameraRole, StreamingRole
7
+
8
+
9
+ class CameraInputSettings(TypedDict):
10
+ _id: str
11
+ name: str
12
+ role: CameraRole
13
+ useForSnapshot: bool
14
+ hotMode: bool
15
+ preload: bool
16
+ muted: NotRequired[bool]
17
+ urls: list[str]
18
+ childSourceId: NotRequired[str]
19
+ childCameraId: NotRequired[str]
20
+ childCameraRole: NotRequired[StreamingRole]
21
+
22
+
23
+ class CameraConfigPartial(TypedDict, total=False):
24
+ name: str
25
+ nativeId: str
26
+ isCloud: bool
27
+ disabled: bool
28
+ info: CameraInformation
29
+ sources: list[CameraConfigInputSettings]
@@ -3,7 +3,5 @@ from __future__ import annotations
3
3
  from typing import Literal
4
4
 
5
5
  FrameType = Literal["stream", "motion"]
6
- """Frame type identifier for frame workers."""
7
6
 
8
7
  CameraFrameWorkerDecoder = Literal["wasm", "rust"]
9
- """Frame worker decoder implementation."""
@@ -7,9 +7,5 @@ from ..camera.events import DetectionEvent
7
7
 
8
8
 
9
9
  class DetectionEventMessage(TypedDict):
10
- """Detection event message published via NATS."""
11
-
12
10
  type: DetectionEventType
13
- """Event lifecycle type."""
14
11
  data: DetectionEvent
15
- """Full event data."""
@@ -7,18 +7,11 @@ from typing import Any
7
7
 
8
8
 
9
9
  class AsyncEventEmitter:
10
- """Event-listener registry that accepts sync and async handlers.
11
-
12
- Listeners are keyed by event name and invoked in registration order. Async
13
- handlers are scheduled fire-and-forget and tracked so :meth:`cancel` can stop them.
14
- """
15
-
16
10
  def __init__(self) -> None:
17
11
  self._events: dict[str, OrderedDict[Callable[..., Any], Callable[..., Any]]] = {}
18
12
  self._waiting: set[asyncio.Future[Any]] = set()
19
13
 
20
14
  def on(self, event: str, f: Callable[..., Any]) -> Callable[..., Any]:
21
- """Register a listener (sync or async) that is invoked for every emission of *event*."""
22
15
  event = self._normalize_event(event)
23
16
  listeners = self._events.get(event)
24
17
  if listeners is None:
@@ -28,7 +21,6 @@ class AsyncEventEmitter:
28
21
  return f
29
22
 
30
23
  def once(self, event: str, f: Callable[..., Any]) -> Callable[..., Any]:
31
- """Register a one-shot listener that runs on the next emission of *event*, then removes itself."""
32
24
  event = self._normalize_event(event)
33
25
 
34
26
  def wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -43,11 +35,6 @@ class AsyncEventEmitter:
43
35
  return f
44
36
 
45
37
  def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
46
- """Invoke every listener registered for *event* with the given arguments.
47
-
48
- Returns ``True`` if *event* had listeners, ``False`` otherwise.
49
- Async handlers are scheduled as fire-and-forget tasks.
50
- """
51
38
  event = self._normalize_event(event)
52
39
  listeners = self._events.get(event)
53
40
  if not listeners:
@@ -70,13 +57,6 @@ class AsyncEventEmitter:
70
57
  on_error: Callable[[BaseException], None] | None = None,
71
58
  **kwargs: Any,
72
59
  ) -> bool:
73
- """Invoke every listener registered for *event* and wait for async handlers to settle.
74
-
75
- Handler exceptions go to *on_error* instead of propagating, so one failing
76
- listener never blocks the others. Returns ``False`` if handlers were still
77
- pending after *timeout* seconds, they keep running and stay cancellable
78
- via :meth:`cancel`.
79
- """
80
60
  event = self._normalize_event(event)
81
61
  listeners = self._events.get(event)
82
62
 
@@ -107,7 +87,6 @@ class AsyncEventEmitter:
107
87
  return not still_pending
108
88
 
109
89
  def remove_listener(self, event: str, f: Callable[..., Any]) -> None:
110
- """Remove a previously registered listener *f* from *event*. No-op if it is not registered."""
111
90
  event = self._normalize_event(event)
112
91
  listeners = self._events.get(event)
113
92
  if listeners is not None:
@@ -116,19 +95,16 @@ class AsyncEventEmitter:
116
95
  del self._events[event]
117
96
 
118
97
  def remove_all_listeners(self, event: str | None = None) -> None:
119
- """Remove every listener for *event*, or every listener for every event when *event* is ``None``."""
120
98
  if event is not None:
121
99
  self._events.pop(self._normalize_event(event), None)
122
100
  else:
123
101
  self._events.clear()
124
102
 
125
103
  def cancel(self) -> None:
126
- """Cancel all pending async tasks."""
127
104
  for future in self._waiting:
128
105
  future.cancel()
129
106
  self._waiting.clear()
130
107
 
131
108
  @staticmethod
132
109
  def _normalize_event(event: Any) -> str:
133
- """Accept both plain event names and enum members."""
134
110
  return event.value if hasattr(event, "value") else str(event)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, NotRequired
4
+
5
+ from ..manager import DiscoveredCamera
6
+
7
+ ConnectionStatus = Literal["idle", "connecting", "connected", "error"]
8
+
9
+
10
+ class DiscoveredCameraWithState(DiscoveredCamera):
11
+ provider: str
12
+
13
+ connectionStatus: ConnectionStatus
14
+
15
+ errorMessage: NotRequired[str]
@@ -54,68 +54,39 @@ if TYPE_CHECKING:
54
54
 
55
55
 
56
56
  class PropertyChangedEvent(TypedDict):
57
- """Emitted when a sensor property value changes."""
58
-
59
57
  sensorId: str
60
- """Sensor that changed."""
61
58
  sensorType: SensorType
62
- """Type of the sensor that changed."""
63
59
  property: str
64
- """Property key that changed."""
65
60
  value: object
66
- """New value."""
67
- previousValue: NotRequired[object]
68
- """Not sent by the server, keep the last value yourself if you need it."""
69
- timestamp: int
70
- """Change time in epoch milliseconds."""
61
+ previousValue: NotRequired[object] # never sent by the server
62
+ timestamp: int # epoch ms
71
63
 
72
64
 
73
65
  PropertyUpdateFn = Callable[[dict[str, Any]], None]
74
- """Receives a partial-state delta, one call per ``_write_state``."""
75
66
 
76
67
  CapabilityUpdateFn = Callable[[list[str]], None]
77
- """Receives the full capability list whenever it changes."""
78
68
 
79
69
 
80
70
  class SensorSourcePatch(TypedDict):
81
- """Source fields a sensor reports to the registry, only the changed ones."""
82
-
83
71
  sourceState: NotRequired[SensorSourceState]
84
72
  address: NotRequired[str]
85
73
 
86
74
 
87
75
  SourceUpdateFn = Callable[[SensorSourcePatch], None]
88
- """Receives a source patch, one call per ``setSourceState`` / ``setAddress``."""
89
76
 
90
77
 
91
78
  class SensorJSON(TypedDict):
92
- """JSON-serializable representation of a sensor for RPC transport."""
93
-
94
79
  id: str
95
- """Sensor ID."""
96
80
  type: SensorType
97
- """Sensor type."""
98
81
  name: str
99
- """Internal name."""
100
82
  displayName: str
101
- """Name shown in the UI."""
102
83
  category: SensorCategory
103
- """Category the sensor belongs to."""
104
84
  nativeId: NotRequired[str]
105
- """Device ID assigned by the plugin."""
106
85
  origin: NotRequired[str]
107
- """Source system the sensor was imported from, e.g. ``'homeassistant'``."""
108
86
  pluginId: NotRequired[str]
109
- """Plugin that owns the sensor."""
110
87
  properties: dict[str, Any]
111
- """Current property values."""
112
88
  capabilities: NotRequired[list[str]]
113
- """Capability keys the sensor reports."""
114
89
  requiresFrames: NotRequired[bool]
115
- """Sensor needs a frame feed to work."""
116
90
  modelSpec: NotRequired[ModelSpec]
117
- """Model the sensor runs, for ML-backed sensors."""
118
91
  sourceState: NotRequired[SensorSourceState]
119
- """The plugin's view of the source behind an adopted sensor."""
120
92
  address: NotRequired[str]
121
- """Current address at the source, e.g. a Home Assistant entity id."""
@@ -4,21 +4,6 @@ from typing import Any
4
4
 
5
5
 
6
6
  def is_equal(first: Any, second: Any, ignore_order: bool = False) -> bool:
7
- """Deep equality for primitives, lists, and dicts. Dict key order never matters.
8
-
9
- Args:
10
- first: First value to compare.
11
- second: Second value to compare.
12
- ignore_order: Compare lists as multisets instead of position by position.
13
-
14
- Returns:
15
- ``True`` if the values are deeply equal.
16
-
17
- Example:
18
- ```python
19
- is_equal({"a": 1, "b": 2}, {"b": 2, "a": 1}) # True
20
- ```
21
- """
22
7
  if first is second:
23
8
  return True
24
9
 
@@ -6,11 +6,6 @@ from typing_extensions import TypedDict
6
6
 
7
7
 
8
8
  class IceServer(TypedDict):
9
- """WebRTC ICE server configuration."""
10
-
11
9
  urls: list[str]
12
- """STUN/TURN server URLs."""
13
10
  username: NotRequired[str]
14
- """Authentication username."""
15
11
  credential: NotRequired[str]
16
- """Authentication credential."""
@@ -48,6 +48,8 @@ class AssistantAskRequest(TypedDict, total=False):
48
48
  images: list[AssistantAskImage]
49
49
  outputSchema: dict[str, Any]
50
50
  timeoutMs: int
51
+ modelId: str
52
+ """Id of a configured model to answer with instead of the assigned one. The plugin still needs access, an unknown id falls back to the assigned model."""
51
53
 
52
54
 
53
55
  class AssistantAskResult(TypedDict, total=False):
@@ -269,7 +269,6 @@ class Subject(Generic[T]):
269
269
  return Observable(lambda cb: self.subscribe(cb))
270
270
 
271
271
  def _on_complete(self, handler: Callable[[], None]) -> Disposable:
272
- """Register a handler run once on completion, immediately if already completed."""
273
272
  if self._completed:
274
273
  handler()
275
274
  return Disposable(lambda: None)
@@ -583,10 +583,6 @@ class Sensor(ABC, Generic[TProperties, TStorage, TCapability]):
583
583
  self._write_state({"modelSpec": spec})
584
584
 
585
585
  def _write_state(self, partial: Mapping[str, Any]) -> None:
586
- """Write changed properties, fire one batched RPC update and notify listeners.
587
-
588
- Used by the semantic helpers on each sensor type, not by plugin code.
589
- """
590
586
  delta: dict[str, Any] = {}
591
587
  changes: list[tuple[str, Any, Any]] = []
592
588
 
@@ -616,18 +612,10 @@ class Sensor(ABC, Generic[TProperties, TStorage, TCapability]):
616
612
  fallback_label: str,
617
613
  fallback_extra: dict[str, Any] | None = None,
618
614
  ) -> list[dict[str, Any]]:
619
- """Normalize the arguments of a `reportDetections(detected, detections)` call.
620
-
621
- - `detected` is False: returns `[]` (clear).
622
- - `detected` is True with detections: returns them, substituting a full-frame box where missing.
623
- - `detected` is True without detections: returns one synthesized full-frame
624
- detection carrying `fallback_label` and `fallback_extra`.
625
- """
626
615
  if not detected:
627
616
  return []
628
617
  if detections:
629
- # smart-camera plugins report labels without coordinates, downstream
630
- # consumers (coordinator, zone matching) require a box on every detection
618
+ # smart-camera plugins report labels without coordinates, consumers need a box on every detection
631
619
  return [
632
620
  detection
633
621
  if detection.get("box")
@@ -647,7 +635,6 @@ class Sensor(ABC, Generic[TProperties, TStorage, TCapability]):
647
635
  self._storage = storage
648
636
 
649
637
  def _fire_lifecycle(self, active: bool) -> None:
650
- """Invoke the lifecycle hook and schedule it if the override is async."""
651
638
  try:
652
639
  result = self.on_start() if active else self.on_stop()
653
640
  except Exception: # noqa: BLE001 - lifecycle errors must not break bookkeeping
@@ -152,8 +152,7 @@ class ClassifierSensor(Sensor[ClassifierSensorProperties, TStorage, str], Generi
152
152
  "motion",
153
153
  {"attribute": "", "subAttribute": ""},
154
154
  )
155
- # the specific answer wins (subAttribute, e.g. the bird species), and labels
156
- # accumulate while the classifier keeps firing, so automations don't flap
155
+ # the specific answer (subAttribute) wins, labels accumulate while firing so automations don't flap
157
156
  recognized = [
158
157
  label for d in list_ if (label := d.get("subAttribute") or d.get("attribute") or d.get("label"))
159
158
  ]
@@ -150,8 +150,7 @@ class FaceSensor(Sensor[FaceSensorProperties, TStorage, str], Generic[TStorage])
150
150
  "person",
151
151
  {"attribute": "face"},
152
152
  )
153
- # recognized names accumulate while faces stay visible, so automations
154
- # don't flap when a face is momentarily unrecognized mid-presence
153
+ # names accumulate while faces stay visible, so automations don't flap on a missed recognition
155
154
  recognized = [d["identity"] for d in list_ if d.get("identity")]
156
155
  state: dict[str, Any] = {
157
156
  FaceProperty.Detected.value: detected,
@@ -156,8 +156,7 @@ class LicensePlateSensor(Sensor[LicensePlateSensorProperties, TStorage, str], Ge
156
156
  "vehicle",
157
157
  {"attribute": "license_plate", "plateText": ""},
158
158
  )
159
- # recognized plates accumulate while plates stay visible, so automations
160
- # don't flap when the OCR misses a frame mid-presence
159
+ # plates accumulate while visible, so automations don't flap when the OCR misses a frame
161
160
  recognized = [d["plateText"] for d in list_ if d.get("plateText")]
162
161
  state: dict[str, Any] = {
163
162
  LicensePlateProperty.Detected.value: detected,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camera-ui-sdk
3
- Version: 1.4.2
3
+ Version: 1.4.4
4
4
  Summary: camera.ui sdk
5
5
  Author-email: seydx <hi@seydx.dev>
6
6
  Maintainer-email: seydx <hi@seydx.dev>
@@ -28,10 +28,10 @@ Provides-Extra: dev
28
28
  Requires-Dist: build>=1.6.1; extra == "dev"
29
29
  Requires-Dist: twine>=7.0.0; extra == "dev"
30
30
  Requires-Dist: mypy>=2.4.0; extra == "dev"
31
- Requires-Dist: ruff>=0.17.1; extra == "dev"
31
+ Requires-Dist: ruff>=0.17.2; extra == "dev"
32
32
  Provides-Extra: docs
33
- Requires-Dist: mkdocs-material>=9.7.7; extra == "docs"
34
- Requires-Dist: mkdocstrings[python]>=1.0.7; extra == "docs"
33
+ Requires-Dist: mkdocs-material>=9.8.0; extra == "docs"
34
+ Requires-Dist: mkdocstrings[python]>=1.0.8; extra == "docs"
35
35
  Requires-Dist: mkdocs>=1.6.1; extra == "docs"
36
36
  Requires-Dist: mkdocs-exclude>=1.0.2; extra == "docs"
37
37
  Dynamic: license-file
@@ -4,11 +4,11 @@ typing_extensions>=4.16.0
4
4
  build>=1.6.1
5
5
  twine>=7.0.0
6
6
  mypy>=2.4.0
7
- ruff>=0.17.1
7
+ ruff>=0.17.2
8
8
 
9
9
  [docs]
10
- mkdocs-material>=9.7.7
11
- mkdocstrings[python]>=1.0.7
10
+ mkdocs-material>=9.8.0
11
+ mkdocstrings[python]>=1.0.8
12
12
  mkdocs>=1.6.1
13
13
  mkdocs-exclude>=1.0.2
14
14
 
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools>=84.0.0"]
2
+ requires = ["setuptools>=84.2.1"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "camera-ui-sdk"
7
- version = "1.4.2"
7
+ version = "1.4.4"
8
8
  description = "camera.ui sdk"
9
9
  keywords = ["camera.ui", "python", "sdk"]
10
10
  readme = "README.md"
@@ -32,11 +32,11 @@ dev = [
32
32
  "build>=1.6.1",
33
33
  "twine>=7.0.0",
34
34
  "mypy>=2.4.0",
35
- "ruff>=0.17.1",
35
+ "ruff>=0.17.2",
36
36
  ]
37
37
  docs = [
38
- "mkdocs-material>=9.7.7",
39
- "mkdocstrings[python]>=1.0.7",
38
+ "mkdocs-material>=9.8.0",
39
+ "mkdocstrings[python]>=1.0.8",
40
40
  "mkdocs>=1.6.1",
41
41
  "mkdocs-exclude>=1.0.2",
42
42
  ]
@@ -1,5 +1,3 @@
1
- """Tests for AsyncEventEmitter."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  import asyncio
@@ -8,8 +6,6 @@ import pytest
8
6
 
9
7
  from camera_ui_sdk.internal import AsyncEventEmitter
10
8
 
11
- # ── on + emit (sync) ─────────────────────────────────────────────────
12
-
13
9
 
14
10
  class TestOnEmitSync:
15
11
  def test_sync_handler_called(self) -> None:
@@ -30,9 +26,6 @@ class TestOnEmitSync:
30
26
  assert results == [1, 2]
31
27
 
32
28
 
33
- # ── on + emit (async) ────────────────────────────────────────────────
34
-
35
-
36
29
  class TestOnEmitAsync:
37
30
  @pytest.mark.asyncio
38
31
  async def test_async_handler_fire_and_forget(self) -> None:
@@ -45,14 +38,10 @@ class TestOnEmitAsync:
45
38
  ee.on("test", handler)
46
39
  ee.emit("test", "async_val")
47
40
 
48
- # Give the scheduled coroutine a chance to run
49
41
  await asyncio.sleep(0)
50
42
  assert results == ["async_val"]
51
43
 
52
44
 
53
- # ── once ──────────────────────────────────────────────────────────────
54
-
55
-
56
45
  class TestOnce:
57
46
  def test_fires_only_once(self) -> None:
58
47
  ee = AsyncEventEmitter()
@@ -80,9 +69,6 @@ class TestOnce:
80
69
  assert results == ["once", "on", "on"]
81
70
 
82
71
 
83
- # ── remove_listener ──────────────────────────────────────────────────
84
-
85
-
86
72
  class TestRemoveListener:
87
73
  def test_removes_specific_handler(self) -> None:
88
74
  ee = AsyncEventEmitter()
@@ -103,7 +89,7 @@ class TestRemoveListener:
103
89
 
104
90
  def test_remove_nonexistent_is_noop(self) -> None:
105
91
  ee = AsyncEventEmitter()
106
- ee.remove_listener("no_such_event", lambda: None) # should not raise
92
+ ee.remove_listener("no_such_event", lambda: None)
107
93
 
108
94
  def test_remove_once_listener(self) -> None:
109
95
  ee = AsyncEventEmitter()
@@ -120,9 +106,6 @@ class TestRemoveListener:
120
106
  assert count == 0
121
107
 
122
108
 
123
- # ── remove_all_listeners ─────────────────────────────────────────────
124
-
125
-
126
109
  class TestRemoveAllListeners:
127
110
  def test_removes_for_specific_event(self) -> None:
128
111
  ee = AsyncEventEmitter()
@@ -153,9 +136,6 @@ class TestRemoveAllListeners:
153
136
  assert results == []
154
137
 
155
138
 
156
- # ── emit return value ────────────────────────────────────────────────
157
-
158
-
159
139
  class TestEmitReturn:
160
140
  def test_returns_true_with_listeners(self) -> None:
161
141
  ee = AsyncEventEmitter()
@@ -167,9 +147,6 @@ class TestEmitReturn:
167
147
  assert ee.emit("evt") is False
168
148
 
169
149
 
170
- # ── cancel ────────────────────────────────────────────────────────────
171
-
172
-
173
150
  class TestCancel:
174
151
  @pytest.mark.asyncio
175
152
  async def test_cancels_pending_tasks(self) -> None:
@@ -184,7 +161,6 @@ class TestCancel:
184
161
  ee.on("evt", long_handler)
185
162
  ee.emit("evt")
186
163
 
187
- # Let the task start
188
164
  await asyncio.sleep(0)
189
165
  assert started is True
190
166
  assert len(ee._waiting) == 1 # pyright: ignore[reportPrivateUsage]
@@ -202,7 +178,7 @@ class TestCancel:
202
178
  ee.on("evt", quick_handler)
203
179
  ee.emit("evt")
204
180
 
205
- # Let the task complete + done callback fire
181
+ # one tick for the task, one for its done callback
206
182
  await asyncio.sleep(0)
207
183
  await asyncio.sleep(0)
208
184
  assert len(ee._waiting) == 0 # pyright: ignore[reportPrivateUsage]
@@ -1,5 +1,3 @@
1
- """Tests for lightweight reactive primitives."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  import asyncio
@@ -21,8 +19,6 @@ from camera_ui_sdk.observable import (
21
19
  share,
22
20
  )
23
21
 
24
- # ── Disposable ────────────────────────────────────────────────────────
25
-
26
22
 
27
23
  class TestDisposable:
28
24
  def test_calls_teardown_once(self) -> None:
@@ -55,9 +51,6 @@ class TestDisposable:
55
51
  assert d.closed is True
56
52
 
57
53
 
58
- # ── Subject ───────────────────────────────────────────────────────────
59
-
60
-
61
54
  class TestSubject:
62
55
  def test_emits_values(self) -> None:
63
56
  subject: Subject[int] = Subject()
@@ -126,9 +119,6 @@ class TestSubject:
126
119
  assert isinstance(obs, Observable)
127
120
 
128
121
 
129
- # ── BehaviorSubject ───────────────────────────────────────────────────
130
-
131
-
132
122
  class TestBehaviorSubject:
133
123
  def test_emits_current_value_on_subscribe(self) -> None:
134
124
  subject = BehaviorSubject(42)
@@ -176,9 +166,6 @@ class TestBehaviorSubject:
176
166
  assert values == []
177
167
 
178
168
 
179
- # ── ReplaySubject ─────────────────────────────────────────────────────
180
-
181
-
182
169
  class TestReplaySubject:
183
170
  def test_replays_buffered_values(self) -> None:
184
171
  subject: ReplaySubject[int] = ReplaySubject(2)
@@ -224,9 +211,6 @@ class TestReplaySubject:
224
211
  assert values == [1]
225
212
 
226
213
 
227
- # ── Observable ────────────────────────────────────────────────────────
228
-
229
-
230
214
  class TestObservable:
231
215
  def test_subscribe_calls_factory(self) -> None:
232
216
  called = False
@@ -252,9 +236,6 @@ class TestObservable:
252
236
  assert values == [1]
253
237
 
254
238
 
255
- # ── Operators ─────────────────────────────────────────────────────────
256
-
257
-
258
239
  class TestFilterOp:
259
240
  def test_filters_values(self) -> None:
260
241
  subject: Subject[int] = Subject()
@@ -428,9 +409,6 @@ class TestShare:
428
409
  assert subscribe_count == 2
429
410
 
430
411
 
431
- # ── Pipe chaining ─────────────────────────────────────────────────────
432
-
433
-
434
412
  class TestPipeChaining:
435
413
  def test_chains_multiple_operators(self) -> None:
436
414
  subject: Subject[int] = Subject()
@@ -465,9 +443,6 @@ class TestPipeChaining:
465
443
  assert values == [0, 1, 2]
466
444
 
467
445
 
468
- # ── firstValueFrom ───────────────────────────────────────────────────
469
-
470
-
471
446
  class TestFirstValueFrom:
472
447
  @pytest.mark.asyncio
473
448
  async def test_resolves_with_first_value(self) -> None:
@@ -1,50 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import NotRequired, TypedDict
4
-
5
- from ..camera.config import CameraConfigInputSettings, CameraInformation
6
- from ..camera.enums import CameraRole, StreamingRole
7
-
8
-
9
- class CameraInputSettings(TypedDict):
10
- """Camera input settings (user configuration)."""
11
-
12
- _id: str
13
- """Unique source ID."""
14
- name: str
15
- """Source display name."""
16
- role: CameraRole
17
- """Resolution role."""
18
- useForSnapshot: bool
19
- """Use this source for snapshots."""
20
- hotMode: bool
21
- """Keep connection always active."""
22
- preload: bool
23
- """Keep a keyframe cache for this source, so the view opens faster. Use ``hotMode`` to keep the stream connected."""
24
- muted: NotRequired[bool]
25
- """Strip the audio track from this source (defaults to False)."""
26
- urls: list[str]
27
- """User-provided stream URLs."""
28
- childSourceId: NotRequired[str]
29
- """Child source ID (for snapshot fallback)."""
30
- childCameraId: NotRequired[str]
31
- """Camera whose stream shows in the picture-in-picture overlay, instead of a source of this camera."""
32
- childCameraRole: NotRequired[StreamingRole]
33
- """Role of the ``childCameraId`` stream shown in the overlay."""
34
-
35
-
36
- class CameraConfigPartial(TypedDict, total=False):
37
- """Camera configuration subset for partial updates."""
38
-
39
- name: str
40
- """Camera display name."""
41
- nativeId: str
42
- """Native device ID from plugin."""
43
- isCloud: bool
44
- """Whether camera streams from cloud."""
45
- disabled: bool
46
- """Disable this camera."""
47
- info: CameraInformation
48
- """Camera hardware information."""
49
- sources: list[CameraConfigInputSettings]
50
- """Video input sources."""
@@ -1,24 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Literal, NotRequired
4
-
5
- from ..manager import DiscoveredCamera
6
-
7
- ConnectionStatus = Literal["idle", "connecting", "connected", "error"]
8
- """Connection status for discovered cameras."""
9
-
10
-
11
- class DiscoveredCameraWithState(DiscoveredCamera):
12
- """Discovered camera plus the provider that found it and its live connection state.
13
-
14
- Used by the UI to render the adoption list.
15
- """
16
-
17
- provider: str
18
- """Name of the provider plugin that discovered this camera."""
19
-
20
- connectionStatus: ConnectionStatus
21
- """Current connection status reported by the provider."""
22
-
23
- errorMessage: NotRequired[str]
24
- """Last error message when ``connectionStatus`` is ``error``."""
File without changes
File without changes
File without changes