rtls-sdk 0.3.0__tar.gz → 0.4.0__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 (101) hide show
  1. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/CHANGELOG.md +76 -0
  2. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/PKG-INFO +1 -1
  3. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/pyproject.toml +1 -1
  4. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/__init__.py +22 -1
  5. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_client.py +6 -0
  6. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/errors.py +20 -0
  7. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/__init__.py +11 -0
  8. rtls_sdk-0.4.0/src/rtls_sdk/models/lsb.py +129 -0
  9. rtls_sdk-0.4.0/src/rtls_sdk/models/network.py +43 -0
  10. rtls_sdk-0.4.0/src/rtls_sdk/models/node_params.py +51 -0
  11. rtls_sdk-0.4.0/src/rtls_sdk/models/schedule.py +30 -0
  12. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/__init__.py +6 -0
  13. rtls_sdk-0.4.0/src/rtls_sdk/resources/lsb.py +373 -0
  14. rtls_sdk-0.4.0/src/rtls_sdk/resources/network.py +54 -0
  15. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/nodes.py +53 -1
  16. rtls_sdk-0.4.0/src/rtls_sdk/resources/schedules.py +92 -0
  17. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/.gitignore +0 -0
  18. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/LICENSE +0 -0
  19. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/README.md +0 -0
  20. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/prompts/README.md +0 -0
  21. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_auth.py +0 -0
  22. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_envelope.py +0 -0
  23. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_http.py +0 -0
  24. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_logging.py +0 -0
  25. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_pagination.py +0 -0
  26. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_query.py +0 -0
  27. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_time.py +0 -0
  28. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_ws.py +0 -0
  29. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/__init__.py +0 -0
  30. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/auth.py +0 -0
  31. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/context.py +0 -0
  32. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/groups.py +0 -0
  33. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/nodes.py +0 -0
  34. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/reports.py +0 -0
  35. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/system.py +0 -0
  36. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/tags.py +0 -0
  37. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/users.py +0 -0
  38. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/zones.py +0 -0
  39. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/_base.py +0 -0
  40. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/alarm.py +0 -0
  41. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/anchor.py +0 -0
  42. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/area.py +0 -0
  43. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/association.py +0 -0
  44. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/bulk.py +0 -0
  45. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/company.py +0 -0
  46. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/csv_blob.py +0 -0
  47. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/floorplan.py +0 -0
  48. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/group.py +0 -0
  49. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/heatmap.py +0 -0
  50. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/import_result.py +0 -0
  51. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/node.py +0 -0
  52. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/notification.py +0 -0
  53. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/position.py +0 -0
  54. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/project.py +0 -0
  55. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/pws.py +0 -0
  56. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/report.py +0 -0
  57. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/session_context.py +0 -0
  58. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/site.py +0 -0
  59. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/subscriber.py +0 -0
  60. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/system.py +0 -0
  61. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/system_health.py +0 -0
  62. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/tag.py +0 -0
  63. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/tag_template.py +0 -0
  64. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/user.py +0 -0
  65. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/__init__.py +0 -0
  66. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/_base.py +0 -0
  67. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/alarm.py +0 -0
  68. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/notify.py +0 -0
  69. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/ota.py +0 -0
  70. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/param_p.py +0 -0
  71. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/position.py +0 -0
  72. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/sensor.py +0 -0
  73. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/user_msg.py +0 -0
  74. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/zone_event.py +0 -0
  75. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/zone.py +0 -0
  76. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/zone_event.py +0 -0
  77. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/py.typed +0 -0
  78. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/_base.py +0 -0
  79. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/alarms.py +0 -0
  80. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/anchors.py +0 -0
  81. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/areas.py +0 -0
  82. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/associations.py +0 -0
  83. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/auth.py +0 -0
  84. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/companies.py +0 -0
  85. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/context.py +0 -0
  86. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/events.py +0 -0
  87. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/floorplans.py +0 -0
  88. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/groups.py +0 -0
  89. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/logger.py +0 -0
  90. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/messaging.py +0 -0
  91. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/notifications.py +0 -0
  92. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/projects.py +0 -0
  93. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/reports.py +0 -0
  94. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/sites.py +0 -0
  95. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/subscribers.py +0 -0
  96. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/system.py +0 -0
  97. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/tags.py +0 -0
  98. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/user_messages.py +0 -0
  99. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/users.py +0 -0
  100. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/ws.py +0 -0
  101. {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/zones.py +0 -0
@@ -7,6 +7,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] — 2026-05-19
11
+
12
+ ### Added
13
+
14
+ #### Schedules, LSB, Network resources (ported from desktop site-plan)
15
+
16
+ - `client.schedules` — CRUD on `/api/v2/tdoa/schedules/`: `list`,
17
+ `get`, `create`, `update`, `delete`. The per-slot table is carried
18
+ as an opaque `content` blob so callers can round-trip both v1 and
19
+ v2 schedule wire formats without an SDK release. New
20
+ `rtls_sdk.Schedule` model.
21
+ - `client.lsb` — read/write the Location Synchronization Bus
22
+ settings on `/api/v2/tdoa/settings/`: `get`, `update`. Settings
23
+ include the active-schedule pointer, autostart flag, and per-anchor
24
+ sync roles (master / distributor / client). The server upserts on
25
+ POST keyed by `project_uid` in the body. The `values` payload is
26
+ exposed as `dict[str, Any]` to support both `lsb_version=1` (`sfd`,
27
+ `autostart_devices`) and `lsb_version=2` (`masters`,
28
+ `distributors`) shapes side-by-side. The SDK also merges the
29
+ top-level `tdoa_version` / `lsb_version` fields from the response
30
+ into the model. New `rtls_sdk.LsbSettings` model.
31
+ - `client.network` — `/api/v2/network_settings/` read/update for the
32
+ per-project service-mode toggle (`enabled`, `duration`,
33
+ `devices_in_service_mode`). New `rtls_sdk.NetworkSettings` and
34
+ `rtls_sdk.ServiceMode` models; null device lists coerce to `[]`.
35
+ - Live tests under `tests/live/test_schedules.py`,
36
+ `tests/live/test_lsb.py`, `tests/live/test_network.py`. The
37
+ service-mode toggle test is guarded by
38
+ `RTLS_LIVE_ALLOW_SERVICE_MODE_TOGGLE=1` because it affects real
39
+ hardware.
40
+ - Docs: `docs/entities/schedules.md`, `lsb.md`, `network.md`.
41
+
42
+ #### LSB start/stop orchestration
43
+
44
+ - `client.lsb.start(masters, *, bcs=1, tdoas=0, timeout=9.0,
45
+ resend_interval=3.0, check_interval=0.7)` — push `sync_slot` + `sf`
46
+ to each master and poll `hw_params` until every node echoes back
47
+ the configured values. Raises `LsbConvergenceError` (with the
48
+ unresolved MAC list) on timeout. Faithful port of the desktop
49
+ site-plan's start flow.
50
+ - `client.lsb.stop(macs, *, timeout=15.0, …)` — send the all-zero
51
+ `sf` "stop trick" to each master and poll until every node reports
52
+ `sf.value.sfd == 0` (or `addr` empty / `"0000"`).
53
+ - New `LsbMasterSpec` dataclass (`mac`, `addr`, `slot`, `sfd`) for
54
+ the `start()` argument.
55
+ - New `LsbConvergenceError` exception carrying `unresolved: list[str]`.
56
+
57
+ #### LSB settings accessors and configured-start convenience
58
+
59
+ - `LsbSettings.masters()` and `LsbSettings.distributors()` — typed
60
+ views over the opaque `values` dict that smooth the v1/v2 fork.
61
+ Return frozen `LsbMasterEntry` / `LsbDistributorEntry` dataclasses
62
+ (also exported at the top level). The wire shape of `values` stays
63
+ an opaque dict for forward-compatibility.
64
+ - `client.lsb.start_configured(*, tdoas, bcs=None, …)` — convenience
65
+ start: reads LSB settings, joins anchors + nodes, derives each
66
+ master's MAC and 4-char hex addr (from `node.node_id`), then
67
+ delegates to `client.lsb.start([...])`. Raises `RuntimeError` with
68
+ a clear message when resolution fails (no masters configured, no
69
+ registered node for an anchor's MAC, etc.). `bcs` defaults to
70
+ `max(slot) + 1` matching site-plan.
71
+
72
+ #### Node parameter primitives
73
+
74
+ - `client.nodes.send_param(mac, **plist)` — POSTs to
75
+ `/api/v2/win_message_que/create.json/`. Accepts any plist keys
76
+ (`sf`, `sync_slot`, `nwk_cfg`, `srv_mode`, `tdoa_schedule`,
77
+ `tdoa_schd`, `p`). Multiple keys per call supported. MAC is
78
+ normalised.
79
+ - `client.nodes.read_params(macs, fields, *, force_update=False)` —
80
+ GETs `/api/v2/hw_params.json` with repeated `mac=` and `fields=`
81
+ query parameters (matching the desktop client's wire format).
82
+ Returns `list[NodeParams]`. Server-side `"pending"` strings on
83
+ each field coerce to `None` so callers always see `dict | None`.
84
+ - New `rtls_sdk.NodeParams` model.
85
+
10
86
  ## [0.3.0] — 2026-05-13
11
87
 
12
88
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rtls-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for the RTLS REST API
5
5
  Project-URL: Homepage, https://github.com/rpplabs/rtls-sdk
6
6
  Project-URL: Documentation, https://github.com/rpplabs/rtls-sdk/tree/master/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rtls-sdk"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Python SDK for the RTLS REST API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -18,6 +18,7 @@ from .errors import (
18
18
  AuthenticationError,
19
19
  Conflict,
20
20
  ConnectionError,
21
+ LsbConvergenceError,
21
22
  NotFound,
22
23
  PartialFailureError,
23
24
  PermissionDenied,
@@ -46,7 +47,12 @@ from .models import (
46
47
  Group,
47
48
  Host,
48
49
  ImportResult,
50
+ LsbDistributorEntry,
51
+ LsbMasterEntry,
52
+ LsbSettings,
53
+ NetworkSettings,
49
54
  Node,
55
+ NodeParams,
50
56
  NotificationProfile,
51
57
  NotificationType,
52
58
  Position,
@@ -54,6 +60,8 @@ from .models import (
54
60
  Report,
55
61
  ReportType,
56
62
  Role,
63
+ Schedule,
64
+ ServiceMode,
57
65
  Site,
58
66
  Subscriber,
59
67
  SubscriberAddresses,
@@ -81,13 +89,18 @@ from .models.ws import (
81
89
  WsChannelMessage,
82
90
  ZoneEventMessage,
83
91
  )
84
- from .resources import UserMessagesAPI, WsAPI
92
+ from .resources import LsbAPI, NetworkAPI, SchedulesAPI, UserMessagesAPI, WsAPI
93
+ from .resources.lsb import LsbMasterSpec
85
94
  from .resources.ws import WsSession
86
95
 
87
96
  __all__ = [ # noqa: RUF022 — grouped logically, not alphabetically
88
97
  # Client
89
98
  "RtlsClient",
90
99
  # Resource sub-clients
100
+ "LsbAPI",
101
+ "LsbMasterSpec",
102
+ "NetworkAPI",
103
+ "SchedulesAPI",
91
104
  "UserMessagesAPI",
92
105
  "WsAPI",
93
106
  # WebSocket types
@@ -106,6 +119,7 @@ __all__ = [ # noqa: RUF022 — grouped logically, not alphabetically
106
119
  "AuthenticationError",
107
120
  "Conflict",
108
121
  "ConnectionError",
122
+ "LsbConvergenceError",
109
123
  "NotFound",
110
124
  "PartialFailureError",
111
125
  "PermissionDenied",
@@ -133,7 +147,12 @@ __all__ = [ # noqa: RUF022 — grouped logically, not alphabetically
133
147
  "Group",
134
148
  "Host",
135
149
  "ImportResult",
150
+ "LsbDistributorEntry",
151
+ "LsbMasterEntry",
152
+ "LsbSettings",
153
+ "NetworkSettings",
136
154
  "Node",
155
+ "NodeParams",
137
156
  "NotificationProfile",
138
157
  "NotificationType",
139
158
  "Position",
@@ -141,6 +160,8 @@ __all__ = [ # noqa: RUF022 — grouped logically, not alphabetically
141
160
  "Report",
142
161
  "ReportType",
143
162
  "Role",
163
+ "Schedule",
164
+ "ServiceMode",
144
165
  "Site",
145
166
  "Subscriber",
146
167
  "SubscriberAddresses",
@@ -30,11 +30,14 @@ from .resources import (
30
30
  FloorplansAPI,
31
31
  GroupsAPI,
32
32
  LoggerAPI,
33
+ LsbAPI,
33
34
  MessagingAPI,
35
+ NetworkAPI,
34
36
  NodesAPI,
35
37
  NotificationsAPI,
36
38
  ProjectsAPI,
37
39
  ReportsAPI,
40
+ SchedulesAPI,
38
41
  SitesAPI,
39
42
  SubscribersAPI,
40
43
  SystemAPI,
@@ -184,6 +187,9 @@ class RtlsClient:
184
187
  self.ws = WsAPI(self)
185
188
  self.logger = LoggerAPI(self)
186
189
  self.context = ContextAPI(self)
190
+ self.schedules = SchedulesAPI(self)
191
+ self.lsb = LsbAPI(self)
192
+ self.network = NetworkAPI(self)
187
193
 
188
194
  # -- public scope helpers --------------------------------------------
189
195
 
@@ -155,6 +155,25 @@ class PartialFailureError(RtlsError):
155
155
  self.partial_result: dict[str, Any] = partial_result or {}
156
156
 
157
157
 
158
+ class LsbConvergenceError(RtlsError):
159
+ """``client.lsb.start()`` / ``stop()`` didn't converge within the timeout.
160
+
161
+ ``unresolved`` lists the MACs that did not reach the target state
162
+ before the timeout elapsed. Inspect to decide whether to retry,
163
+ take individual nodes offline, or accept the partial state.
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ message: str,
169
+ *,
170
+ unresolved: list[str],
171
+ **kwargs: Any,
172
+ ) -> None:
173
+ super().__init__(message, **kwargs)
174
+ self.unresolved = unresolved
175
+
176
+
158
177
  class WsError(RtlsError):
159
178
  """Base class for WebSocket surface errors.
160
179
 
@@ -253,6 +272,7 @@ __all__ = [
253
272
  "AuthenticationError",
254
273
  "Conflict",
255
274
  "ConnectionError",
275
+ "LsbConvergenceError",
256
276
  "NotFound",
257
277
  "PartialFailureError",
258
278
  "PermissionDenied",
@@ -11,12 +11,16 @@ from .floorplan import Floorplan
11
11
  from .group import Group
12
12
  from .heatmap import Heatmap, HeatmapInput
13
13
  from .import_result import ImportResult
14
+ from .lsb import LsbDistributorEntry, LsbMasterEntry, LsbSettings
15
+ from .network import NetworkSettings, ServiceMode
14
16
  from .node import Node
17
+ from .node_params import NodeParams
15
18
  from .notification import NotificationProfile, NotificationType
16
19
  from .position import Position
17
20
  from .project import Project
18
21
  from .pws import PwsEvent, PwsReport
19
22
  from .report import Report, ReportType
23
+ from .schedule import Schedule
20
24
  from .session_context import SessionContext
21
25
  from .site import Site
22
26
  from .subscriber import Subscriber, SubscriberAddresses, SystemSubscriber
@@ -45,7 +49,12 @@ __all__ = [
45
49
  "HeatmapInput",
46
50
  "Host",
47
51
  "ImportResult",
52
+ "LsbDistributorEntry",
53
+ "LsbMasterEntry",
54
+ "LsbSettings",
55
+ "NetworkSettings",
48
56
  "Node",
57
+ "NodeParams",
49
58
  "NotificationProfile",
50
59
  "NotificationType",
51
60
  "Position",
@@ -55,6 +64,8 @@ __all__ = [
55
64
  "Report",
56
65
  "ReportType",
57
66
  "Role",
67
+ "Schedule",
68
+ "ServiceMode",
58
69
  "SessionContext",
59
70
  "Site",
60
71
  "Subscriber",
@@ -0,0 +1,129 @@
1
+ """LSB (Location Synchronization Bus) settings model.
2
+
3
+ LSB settings hold the TDOA sync configuration for a project: which
4
+ anchor is the master, which are distributors, slot assignments,
5
+ superframe frequency, and the uid of the currently active schedule.
6
+
7
+ The shape of ``values`` differs between LSB v1 (``sfd`` /
8
+ ``autostart_devices``) and v2 (``masters`` / ``distributors``). The SDK
9
+ keeps the wire payload as an opaque dict so both versions round-trip
10
+ without forcing typed sub-models on callers, and provides typed
11
+ read-side accessors (:meth:`LsbSettings.masters`,
12
+ :meth:`LsbSettings.distributors`) that smooth the v1/v2 fork.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ from pydantic import Field
21
+
22
+ from ._base import _BaseModel
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LsbMasterEntry:
27
+ """A typed view over one master row in LSB settings.
28
+
29
+ Cross-version: on v1, masters come from ``values.autostart_devices``
30
+ (carrying only a MAC and inheriting the global ``values.sfd``); on
31
+ v2, from ``values.masters[]`` (per-master ``sfd`` and ``slot``). The
32
+ fields that don't apply to a given version are ``None``.
33
+ """
34
+
35
+ sfd: int
36
+ slot: int
37
+ anchor_uid: str | None = None
38
+ mac: str | None = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class LsbDistributorEntry:
43
+ """A typed view over one distributor row (v2 only).
44
+
45
+ On v1 there is no distributor concept and
46
+ :meth:`LsbSettings.distributors` returns an empty list.
47
+ """
48
+
49
+ anchor_uid: str
50
+ slot: int
51
+ master_uid: str | None = None
52
+
53
+
54
+ class LsbSettings(_BaseModel):
55
+ """LSB sync configuration for a project.
56
+
57
+ ``values`` is intentionally untyped — see module docstring. Use
58
+ :meth:`masters` / :meth:`distributors` for typed read access.
59
+ """
60
+
61
+ project_uid: str | None = None
62
+ tdoa_version: int | None = None
63
+ lsb_version: int | None = None
64
+ values: dict[str, Any] = Field(default_factory=dict)
65
+
66
+ def masters(self) -> list[LsbMasterEntry]:
67
+ """Typed view of the configured masters, cross-version.
68
+
69
+ v2 reads ``values.masters[]`` directly. v1 reads
70
+ ``values.autostart_devices[].mac`` and pairs each MAC with the
71
+ global ``values.sfd`` (default 1000 if missing) and a
72
+ positional ``slot``.
73
+ """
74
+ if self.lsb_version == 2:
75
+ raw = self.values.get("masters")
76
+ if not isinstance(raw, list):
77
+ return []
78
+ out: list[LsbMasterEntry] = []
79
+ for row in raw:
80
+ if not isinstance(row, dict):
81
+ continue
82
+ out.append(
83
+ LsbMasterEntry(
84
+ sfd=int(row.get("sfd", 0)),
85
+ slot=int(row.get("slot", 0)),
86
+ anchor_uid=row.get("anchor_uid"),
87
+ )
88
+ )
89
+ return out
90
+ # v1 (or unknown): treat autostart_devices as the master list
91
+ raw_v1 = self.values.get("autostart_devices")
92
+ if not isinstance(raw_v1, list):
93
+ return []
94
+ global_sfd = int(self.values.get("sfd") or 1000)
95
+ out_v1: list[LsbMasterEntry] = []
96
+ for idx, row in enumerate(raw_v1):
97
+ if not isinstance(row, dict):
98
+ continue
99
+ mac = row.get("mac")
100
+ if not isinstance(mac, str):
101
+ continue
102
+ out_v1.append(LsbMasterEntry(sfd=global_sfd, slot=idx, mac=mac))
103
+ return out_v1
104
+
105
+ def distributors(self) -> list[LsbDistributorEntry]:
106
+ """Typed view of the configured distributors. Empty on v1."""
107
+ if self.lsb_version != 2:
108
+ return []
109
+ raw = self.values.get("distributors")
110
+ if not isinstance(raw, list):
111
+ return []
112
+ out: list[LsbDistributorEntry] = []
113
+ for row in raw:
114
+ if not isinstance(row, dict):
115
+ continue
116
+ anchor_uid = row.get("anchor_uid")
117
+ if not isinstance(anchor_uid, str):
118
+ continue
119
+ out.append(
120
+ LsbDistributorEntry(
121
+ anchor_uid=anchor_uid,
122
+ slot=int(row.get("slot", 0)),
123
+ master_uid=row.get("master_uid"),
124
+ )
125
+ )
126
+ return out
127
+
128
+
129
+ __all__ = ["LsbDistributorEntry", "LsbMasterEntry", "LsbSettings"]
@@ -0,0 +1,43 @@
1
+ """Network settings model (project-scoped service-mode config)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import Field, model_validator
8
+
9
+ from ._base import _BaseModel
10
+
11
+
12
+ class ServiceMode(_BaseModel):
13
+ """Service-mode toggle for a project's devices.
14
+
15
+ ``devices_in_service_mode`` is sometimes returned as ``null`` on
16
+ deployments that haven't activated service mode yet; coerce to an
17
+ empty list so the typed surface stays clean.
18
+ """
19
+
20
+ enabled: bool = False
21
+ enabled_at: str | None = None
22
+ duration: int | None = None
23
+ expired: bool = False
24
+ devices_in_service_mode: list[str] = Field(default_factory=list)
25
+
26
+ @model_validator(mode="before")
27
+ @classmethod
28
+ def _coerce_null_devices(cls, data: Any) -> Any:
29
+ if isinstance(data, dict) and data.get("devices_in_service_mode") is None:
30
+ data = {**data, "devices_in_service_mode": []}
31
+ return data
32
+
33
+
34
+ class NetworkSettings(_BaseModel):
35
+ """Per-project network configuration."""
36
+
37
+ project_uid: str | None = None
38
+ created_at: str | None = None
39
+ updated_at: str | None = None
40
+ service_mode: ServiceMode | None = None
41
+
42
+
43
+ __all__ = ["NetworkSettings", "ServiceMode"]
@@ -0,0 +1,51 @@
1
+ """Node parameter snapshot model (server-side parameter cache).
2
+
3
+ Returned by ``client.nodes.read_params(...)``. Each parameter field
4
+ holds either ``None`` (server-side "pending" — node hasn't reported
5
+ the value back yet) or a dict ``{"value": {...}, "elapsed": int}`` with
6
+ the last known value and a timestamp.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import model_validator
14
+
15
+ from ._base import _BaseModel
16
+
17
+
18
+ class NodeParams(_BaseModel):
19
+ """One row of a ``hw_params.json`` response.
20
+
21
+ Field shapes per parameter type (when not ``None``):
22
+
23
+ - ``sf``: ``{"value": {"addr", "sfd", "bcs", "tdoas", "bsd", "tsd"}, "elapsed": int}``
24
+ - ``sync_slot``: ``{"value": {"addr": str, "slot": int}, "elapsed": int}``
25
+ - ``nwk_cfg``: ``{"value": {"router": bool}, "elapsed": int}``
26
+
27
+ Other parameter types (``tdoa_schedule``, ``tdoa_schd``,
28
+ ``srv_mode``, ``p``, …) ride on ``extra="allow"`` and are
29
+ accessible via :attr:`raw`.
30
+ """
31
+
32
+ mac_address: str
33
+ sf: dict[str, Any] | None = None
34
+ sync_slot: dict[str, Any] | None = None
35
+ nwk_cfg: dict[str, Any] | None = None
36
+
37
+ @model_validator(mode="before")
38
+ @classmethod
39
+ def _coerce_pending(cls, data: Any) -> Any:
40
+ """Convert ``"pending"`` strings (server-side cache miss) to ``None``.
41
+
42
+ The server returns ``"sf": "pending"`` for a node it has queued
43
+ but not yet heard back from. Coerce so callers always see
44
+ ``dict | None`` and can branch on truthiness.
45
+ """
46
+ if isinstance(data, dict):
47
+ return {k: (None if v == "pending" else v) for k, v in data.items()}
48
+ return data
49
+
50
+
51
+ __all__ = ["NodeParams"]
@@ -0,0 +1,30 @@
1
+ """TDOA schedule model.
2
+
3
+ A ``Schedule`` is a named TDOA timing plan stored on the server. The
4
+ desktop site-plan tool serializes the per-slot schedule items (source /
5
+ destination anchor, slot number, dimension, margin, …) into the
6
+ ``content`` blob; the SDK keeps it opaque so callers can read or replace
7
+ it without the SDK taking a position on the v1 vs v2 wire format.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ._base import _BaseModel
13
+
14
+
15
+ class Schedule(_BaseModel):
16
+ """A TDOA schedule definition.
17
+
18
+ ``content`` is the serialized per-slot table (format depends on
19
+ ``tdoa_version``). Use :attr:`raw` for any fields the SDK doesn't
20
+ surface yet.
21
+ """
22
+
23
+ uid: str
24
+ name: str | None = None
25
+ content: str | None = None
26
+ default: bool = False
27
+ tdoa_version: int | None = None
28
+
29
+
30
+ __all__ = ["Schedule"]
@@ -11,11 +11,14 @@ from .events import EventsAPI
11
11
  from .floorplans import FloorplansAPI
12
12
  from .groups import GroupsAPI
13
13
  from .logger import LoggerAPI
14
+ from .lsb import LsbAPI
14
15
  from .messaging import MessagingAPI
16
+ from .network import NetworkAPI
15
17
  from .nodes import NodesAPI
16
18
  from .notifications import NotificationsAPI
17
19
  from .projects import ProjectsAPI
18
20
  from .reports import ReportsAPI
21
+ from .schedules import SchedulesAPI
19
22
  from .sites import SitesAPI
20
23
  from .subscribers import SubscribersAPI
21
24
  from .system import SystemAPI
@@ -37,11 +40,14 @@ __all__ = [
37
40
  "FloorplansAPI",
38
41
  "GroupsAPI",
39
42
  "LoggerAPI",
43
+ "LsbAPI",
40
44
  "MessagingAPI",
45
+ "NetworkAPI",
41
46
  "NodesAPI",
42
47
  "NotificationsAPI",
43
48
  "ProjectsAPI",
44
49
  "ReportsAPI",
50
+ "SchedulesAPI",
45
51
  "SitesAPI",
46
52
  "SubscribersAPI",
47
53
  "SystemAPI",