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.
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/CHANGELOG.md +76 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/PKG-INFO +1 -1
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/pyproject.toml +1 -1
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/__init__.py +22 -1
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_client.py +6 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/errors.py +20 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/__init__.py +11 -0
- rtls_sdk-0.4.0/src/rtls_sdk/models/lsb.py +129 -0
- rtls_sdk-0.4.0/src/rtls_sdk/models/network.py +43 -0
- rtls_sdk-0.4.0/src/rtls_sdk/models/node_params.py +51 -0
- rtls_sdk-0.4.0/src/rtls_sdk/models/schedule.py +30 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/__init__.py +6 -0
- rtls_sdk-0.4.0/src/rtls_sdk/resources/lsb.py +373 -0
- rtls_sdk-0.4.0/src/rtls_sdk/resources/network.py +54 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/nodes.py +53 -1
- rtls_sdk-0.4.0/src/rtls_sdk/resources/schedules.py +92 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/.gitignore +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/LICENSE +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/README.md +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/prompts/README.md +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_auth.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_envelope.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_http.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_logging.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_pagination.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_query.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_time.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/_ws.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/__init__.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/auth.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/context.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/groups.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/nodes.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/reports.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/system.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/tags.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/users.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/compounds/zones.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/_base.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/alarm.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/anchor.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/area.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/association.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/bulk.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/company.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/csv_blob.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/floorplan.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/group.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/heatmap.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/import_result.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/node.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/notification.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/position.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/project.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/pws.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/report.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/session_context.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/site.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/subscriber.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/system.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/system_health.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/tag.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/tag_template.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/user.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/__init__.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/_base.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/alarm.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/notify.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/ota.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/param_p.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/position.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/sensor.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/user_msg.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/ws/zone_event.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/zone.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/models/zone_event.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/py.typed +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/_base.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/alarms.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/anchors.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/areas.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/associations.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/auth.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/companies.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/context.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/events.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/floorplans.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/groups.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/logger.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/messaging.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/notifications.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/projects.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/reports.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/sites.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/subscribers.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/system.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/tags.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/user_messages.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/users.py +0 -0
- {rtls_sdk-0.3.0 → rtls_sdk-0.4.0}/src/rtls_sdk/resources/ws.py +0 -0
- {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
|
|
@@ -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",
|