Skip to content

Changelog

All notable changes to the RTLS SDK will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased]

[0.6.1] — 2026-07-07

Fixed

  • Packaging: the source distribution (sdist) no longer bundles internal files. Switched the sdist target to only-include, so the tarball ships only src/, README.md, CHANGELOG.md, LICENSE, and pyproject.toml (previously prompts/ and other repo files leaked into the published tarball).

[0.6.0] — 2026-07-06

Changed

  • Documentation and packaging cleanup for the public PyPI release: removed internal/enterprise references and repository links from the docs and project metadata.

  • Breaking: reworked the LsbAPI orchestration surface to be driven entirely by saved settings + the active schedule. update is renamed to save; start() and stop() are now no-arg and return bool; new status() -> LsbStatus (STARTED/STOPPED/ UNKNOWN) compares the nodes currently reporting as master against the configured masters. start() now persists configured_state = True and stop() persists configured_state = False. Added calc_tdoas(schedule=None) (parses the active v2 schedule → max(slot)+1) and send_schedule(schedule=None) (pushes a per-anchor tdoa_schedule plist). Removed start_configured, the arg-taking start(masters) / stop(macs), and LsbMasterSpec; start/stop no longer raise LsbConvergenceError (they return False on timeout). v2 (TDOA) only.

  • Breaking: renamed the trackable-association surface from Association to TagAssociation to disambiguate it from AnchorAssociation. The resource class AssociationsAPI is now TagAssociationsAPI, the client attribute client.associations is now client.tag_associations, and the SessionContext.associations field (and its errors dict key) is now tag_associations. Module files models/association.py / resources/associations.py were renamed to models/tag_association.py / resources/tag_associations.py. Server wire paths, envelope keys, and the AnchorAssociation surface are unchanged.
  • Breaking: TagAssociationsAPI method arguments renamed for brevity/consistency: mac_addressmac and trackable_uidobj_uid across create, open, close (and bulk_create tuples). The wire body keys (mac_address, obj_uid) and the TagAssociation model fields are unchanged.
  • Breaking: reworked LsbSettings (LSB/TDOA v2 only). The config nested under values is lifted onto typed top-level members on read — active_schedule_uid, autostart, configured_state, and the masters / distributors lists (parsed into the new nested LsbSettings.Master / LsbSettings.Distributor models) — and the raw values blob is dropped. These members are the single source of truth: lsb.update(settings) now takes an LsbSettings and rebuilds values from them. The masters() / distributors() accessor methods, the LsbMasterEntry / LsbDistributorEntry dataclasses, the v1 autostart_devices derivation, and the old update(project_uid, *, values=...) signature were removed. The model module models/lsb.py was renamed to models/lsb_settings.py. LsbSettings (and its nested Master / Distributor) are mutable — unlike the SDK's other (frozen) models — to support the get → edit → update workflow.

Added

  • TagAssociationsAPI.open(mac, obj_uid) — close any open association for the MAC or the trackable, then create a fresh one (clean 1:1 binding); mirrors AnchorAssociationsAPI.open.

[0.5.0] — 2026-05-19

Added

  • client.anchor_associations — node ↔ placed-anchor binding on the /api/v2/win_anchor_associations resource: list, list_all (sends ?all=true), create(mac_address, anchor_uid), update(uid, *, mac_address=..., anchor_uid=...), close(...) (by AnchorAssociation, uid, or kwargs), delete(uid). The server's 9999-12-31T23:59:59.000Z "open" sentinel is mapped to closed_at=None on the new AnchorAssociation model. The wire field is win_uid; the SDK exposes it as anchor_uid on method signatures. Server PUT requires both mac_address and win_uid; partial update() calls do an implicit list_all() lookup to fill the unchanged field.
  • Unit tests in tests/unit/test_resources_write_m3.py (TestAnchorAssociationsWrites) and live tests in tests/live/test_anchor_associations.py (8 scenarios — create / list / list_all / update / close-by-mac / close-by-anchor / delete / duplicate-422 cases).

[0.4.0] — 2026-05-19

Added

Schedules, LSB, Network resources (ported from desktop site-plan)

  • client.schedules — CRUD on /api/v2/tdoa/schedules/: list, get, create, update, delete. The per-slot table is carried as an opaque content blob so callers can round-trip both v1 and v2 schedule wire formats without an SDK release. New rtls_sdk.Schedule model.
  • client.lsb — read/write the Location Synchronization Bus settings on /api/v2/tdoa/settings/: get, update. Settings include the active-schedule pointer, autostart flag, and per-anchor sync roles (master / distributor / client). The server upserts on POST keyed by project_uid in the body. The values payload is exposed as dict[str, Any] to support both lsb_version=1 (sfd, autostart_devices) and lsb_version=2 (masters, distributors) shapes side-by-side. The SDK also merges the top-level tdoa_version / lsb_version fields from the response into the model. New rtls_sdk.LsbSettings model.
  • client.network/api/v2/network_settings/ read/update for the per-project service-mode toggle (enabled, duration, devices_in_service_mode). New rtls_sdk.NetworkSettings and rtls_sdk.ServiceMode models; null device lists coerce to [].
  • Live tests under tests/live/test_schedules.py, tests/live/test_lsb.py, tests/live/test_network.py. The service-mode toggle test is guarded by RTLS_LIVE_ALLOW_SERVICE_MODE_TOGGLE=1 because it affects real hardware.
  • Docs: docs/entities/schedules.md, lsb.md, network.md.

LSB start/stop orchestration

  • client.lsb.start(masters, *, bcs=1, tdoas=0, timeout=9.0, resend_interval=3.0, check_interval=0.7) — push sync_slot + sf to each master and poll hw_params until every node echoes back the configured values. Raises LsbConvergenceError (with the unresolved MAC list) on timeout. Faithful port of the desktop site-plan's start flow.
  • client.lsb.stop(macs, *, timeout=15.0, …) — send the all-zero sf "stop trick" to each master and poll until every node reports sf.value.sfd == 0 (or addr empty / "0000").
  • New LsbMasterSpec dataclass (mac, addr, slot, sfd) for the start() argument.
  • New LsbConvergenceError exception carrying unresolved: list[str].

LSB settings accessors and configured-start convenience

  • LsbSettings.masters() and LsbSettings.distributors() — typed views over the opaque values dict that smooth the v1/v2 fork. Return frozen LsbMasterEntry / LsbDistributorEntry dataclasses (also exported at the top level). The wire shape of values stays an opaque dict for forward-compatibility.
  • client.lsb.start_configured(*, tdoas, bcs=None, …) — convenience start: reads LSB settings, joins anchors + nodes, derives each master's MAC and 4-char hex addr (from node.node_id), then delegates to client.lsb.start([...]). Raises RuntimeError with a clear message when resolution fails (no masters configured, no registered node for an anchor's MAC, etc.). bcs defaults to max(slot) + 1 matching site-plan.

Node parameter primitives

  • client.nodes.send_param(mac, **plist) — POSTs to /api/v2/win_message_que/create.json/. Accepts any plist keys (sf, sync_slot, nwk_cfg, srv_mode, tdoa_schedule, tdoa_schd, p). Multiple keys per call supported. MAC is normalised.
  • client.nodes.read_params(macs, fields, *, force_update=False) — GETs /api/v2/hw_params.json with repeated mac= and fields= query parameters (matching the desktop client's wire format). Returns list[NodeParams]. Server-side "pending" strings on each field coerce to None so callers always see dict | None.
  • New rtls_sdk.NodeParams model.

[0.3.0] — 2026-05-13

Documentation

  • New entities/streaming.md documenting client.ws, the eight typed channel models, reconnects, heartbeats, backpressure, and a REST → WS round-trip recipe. Reference pages (resources.md, models.md, errors.md) now list the WS surface. Catalog and quickstart link to the new page.

Added

Typed WS channel models (M18)

  • rtls_sdk.models.ws — new sub-package with one pydantic model per decoded channel: PositionMessage (pos), NotifyMessage (notify), AlarmMessage (alarm), ZoneEventMessage (zone_event), SensorMessage (sensor), OtaMessage (ota), ParamPMessage (param_p), UserMsgMessage (user_msg). All eight inherit from WsChannelEnvelope and carry the full wire dict on .raw for forward-compat field access.
  • client.ws.subscribe(...) now yields typed messages instead of the M17 untyped envelope; iteration patterns like match msg: case PositionMessage(x=x, y=y): ... work directly. The yield type narrowed from WsChannelMessage to WsChannelEnvelope.
  • Unknown channels and decode failures fall back to WsChannelMessage (now a pydantic model, was a frozen dataclass) so the iterator never dies on a single bad frame. Validation errors log at WARNING with the channel name.
  • New include_heartbeats: bool = False kwarg on client.ws.subscribe(...). By default, notify frames with action == "HB" are dropped at the reader; opt-in callers receive them as NotifyMessage(action="HB"). Heartbeat drops do not count toward session.messages_dropped — that counter remains backpressure-only.

Changed

  • WsChannelMessage moved from rtls_sdk._ws to rtls_sdk.models.ws; the import path under the top-level package (from rtls_sdk import WsChannelMessage) is unchanged.

WebSocket subscription scaffold (M17)

  • client.ws — new WsAPI sub-client wrapping the JSON-RPC 2.0 subscriptions-manager. ws.list_channels() returns the server's advertised channel inventory in a one-shot dial (no auth required). ws.subscribe(channels, *, project_uid=None, reconnect=True, queue_max=1024) opens a long-lived session that is both a context manager and a blocking iterator of WsChannelMessage envelopes. Naming is transport-specific on purpose — future streaming protocols (MQTT, Kafka) would land as siblings (client.mqtt, client.kafka) rather than crowd a generic client.stream namespace.
  • WsChannelMessage(channel, raw) — untyped pass-through for any channel; typed channel models land in M18.
  • Reader runs on a daemon thread behind a bounded queue. Backpressure drops the oldest message and bumps session.messages_dropped. Auto-reconnect with capped exponential backoff (1, 2, 4, 8, 15, 30, 30, 30) seconds on unclean disconnects; re-subscribes the current channel set. Auth failure during reconnect surfaces as WsAuthError from the iterator and stops further attempts.
  • New error classes WsError, WsAuthError, and WsProtocolError (all subclasses of RtlsError).
  • New dependency: websockets >= 12.0.

User messages REST (M16)

  • client.user_messages.send(tag_uid, hex) — new UserMessagesAPI sub-client; sends a hex payload to a tag's badge over the WIN gateway. Wire endpoint: POST /api/v2/user_msg/. Independent of client.messaging (which targets the inbox endpoint at /api/v2/messaging); the two surfaces stay separate because the endpoints serve different features on the wire (UDP frame vs. inbox row) and share no storage. Client-side validation rejects malformed hex before any network I/O.

[0.2.0] — 2026-05-13

Added

Places CRUD (M9)

  • client.sites.create / update / delete — full lifecycle for the wire locations resource. update uses a conservative name / shift allowlist mirroring the JS reference client; create accepts the wider server payload via **fields. Server-side cascade on delete (areas / floorplans / anchors / zones go too).
  • client.areas.create / update / delete — wire sublocations resource. create requires name + location_uid; update passes arbitrary placement fields (position_x, position_y, width, height, rotation) through. Server-side cascade.
  • client.anchors.create / update / delete — wire win_anchors. create enforces name, position, type (ANCHOR or BRIDGE, validated locally); optional locked, acc_config, bt_config, state, device_config ride in **fields.
  • normalize_mac() helper — accepts six input shapes (bare 12-hex upper / lower, : or - separated) and emits the canonical bare-12-hex-lowercase the server's mac_regex requires. Wired through nodes.create / validate / assign_raw / release_raw / release / import_ and associations.create / bulk_create.
  • Live fixtures writable_site_uid / writable_area_uid for destructive live tests against the shared test server, with sdk-test-<uuid4> prefixes and yield-style cleanup.

Floorplans + image lifecycle (M10)

  • client.floorplans.create — base64-in-JSON image upload. Image is required; pass image_bytes + original_filename OR image_path (either-or, enforced with ValueError before any HTTP call).
  • client.floorplans.update — diff-driven; replaces the stored image only when image_bytes / image_path is provided.
  • client.floorplans.delete — removes the row and the stored image.
  • client.floorplans.download_image — 180-second read timeout (overrides the default 30 s) for multi-MB PDFs; optional stream_to=<file> for incremental writes.
  • Server re-encodes PNGs on storage — documented in the entity page; tests compare IHDR dimensions, not byte content.

Fixed

Schema audit (M12)

  • tags.create no longer accepts mac_address. The server's Joi createTrackObject schema has no mac_address field; the default stripUnknown silently dropped it, so the call appeared to succeed but the MAC never bound. Callers who want hardware binding now go through nodes.create + associations.create (the migration guide and docs/entities/tags.md document the three-call replacement).
  • Canonical MAC format is bare 12-hex lowercase. The server's mac_regex rejects any separator; previous SDK responses / examples that showed colon-separated MACs (AA:BB:CC:DD:EE:FF) have been corrected across docs and tests. normalize_mac() (introduced in M9) is now applied at every MAC entry-point as a precondition, not a convenience.
  • Regression gate: a live MAC round-trip test exercises the create → list → release cycle to prove the wire format stays canonical on both sides.

Wire-shape mismatches caught by the M14 full-stack live test

  • companies.create now includes a role on the seeded admin user (defaults to "company_admin"; override via admin_role=). The Joi companyCreate.user.role field is required; previously the SDK omitted it and the server responded "user.role" is required.
  • projects.create / projects.update now wrap the body under {"project": {...}} to match the Joi createProject / updateProject schemas. The previously-flat body was rejected with "project" is required.
  • associations.close now sends mac_address / obj_uid instead of the association's own uid. The server's close handler reads only those two keys from the body; passing {"uid": "..."} raised "No mac or trackable object uid specified.". The signature now accepts an Association object, an association uid (with an extra list() lookup), or explicit mac_address / trackable_uid kwargs. compounds/nodes.release updated to pass the Association object directly.

Each of these has a matching unit-test regression in tests/unit/test_resources_write_m3.py.

Documentation

  • Reorganized user docs around entities instead of workflows (M11). Every entity (tags, sites, areas, …) now has a single page covering methods, examples, model fields, and cross-references — answering "what can I do with X?" in one click from the new docs/entities/ catalog.
  • Deleted docs/guides/reading-data.md, docs/guides/writing-data.md, docs/guides/session-context.md — superseded by entity pages.
  • Trimmed docs/guides/compound-workflows.md to a concept page that points at entity pages for examples.
  • Nav restructure: Entities is now the second section after Quickstart; cross-cutting guides (error handling, scope switching, pagination, …) follow.
  • README rewritten to lead with the entity catalog; previous flat link list (which pointed at the now-deleted reading-data / writing-data / session-context guides) replaced with a grouped Start-here / Cross-cutting / Advanced / Reference structure.

[0.1.0] — initial release

The first release covers the milestones M1–M8 of the original design. The SDK provides a sync surface for the RTLS REST API with built-in session management, scope handling, and compound workflows that collapse multi-call sagas behind a single Python method.

Added

Core client (M1)

  • RtlsClient(username, password, base_url, ...) — construct from explicit credentials.
  • RtlsClient(token, base_url, ...) — bring-your-own token mode.
  • RtlsClient.from_env() — construct from RTLS_USERNAME / RTLS_PASSWORD / RTLS_BASE_URL (+ optional scope vars).
  • Lazy login: no network I/O at construction; the first method call performs POST /auth/log_in.json.
  • Transparent 401 re-authentication with threading.Lock-protected refresh coalescing.
  • Mandatory secret redaction filter for the rtls_sdk named logger (no opt-out). Headers and body keys redacted.
  • Typed exception hierarchy under RtlsError: AuthenticationError, PermissionDenied, NotFound, Conflict, ValidationError, RateLimited, ServerError, ConnectionError.

Read surface (M2)

  • client.tags.list/get, client.sites.list/get, client.areas.list/get, client.floorplans.list/get, client.zones.list/get, client.groups.list/get, client.users.list/get/me, client.nodes.list/get, client.anchors.list/get, client.alarms.list/get, client.events.list/get, client.notifications.list/get, client.subscribers.list/get, client.projects.list/get, client.companies.list/get, client.reports.list/get.
  • client.auth.whoami(), client.auth.capabilities().
  • client.system.host/uptime/connections/version/beta_version/ws_host/helper_link/monitor_link.
  • client.messaging.*, client.logger.*.
  • Typed pydantic v2 models for every returned resource.

Simple writes (M3)

  • create_raw / update_raw / delete_raw for all writable resources — 1:1 wrappers over the REST surface.
  • auth.change_password(old, new) with token invalidation.
  • users.create_raw / update_raw / change_password_raw and related raw flows.

Trackable compounds (M4)

  • tags.create(name, ..., attached_zone=..., groups=...) — POST + zone attach + groups bind with rollback.
  • tags.update, tags.delete (incl. bulk-list form returning BulkResult).
  • tags.bulk_update(uids, changes).
  • zones.create/update/delete with dynamic-binding management.
  • groups.create/update/delete with membership cleanup.
  • associations.bulk_create continue-on-error.

User/Node compounds (M5)

  • users.create(email, password, ..., project_role=...) — user + project-role binding compound.
  • users.update(uid, ..., project_role=...) — with project_role diff semantics (add/remove/replace).
  • nodes.release(node_uid, project_uid=...) — cross-scope release with association close and per-thread scope override.
  • system.health() — aggregated host + uptime + connections snapshot with partial/errors fan-out tracking.

Session context (M6)

  • client.context.load() — the dashboard bootstrap. Fans out ~20 sub-resource reads into one SessionContext snapshot with partial / errors partial-failure semantics.
  • Cross-scope nodes fetch via internal per-thread scope override.

Reports + pagination (M7)

  • client.reports.heatmap(...) — POST + poll until ready, with ReportFailed / ReportTimeout outcomes.
  • client.reports.pws(...) — aggregate PWS event report.
  • client.reports.zone_activity_csv(...) — chunked CSV download following content-next-page, with optional streaming and duplicate-header dedup.
  • client.reports.alarms_csv(...) — alarms CSV with optional multi-line banner prefix.
  • client.events.iter_pws(...) — lazy iterator surface for PWS events; three-style paginator infrastructure (HeaderCursorPaginator, BodyUrlPaginator, PageLimitPaginator).
  • New models: Heatmap, HeatmapInput, PwsReport, PwsEvent, CsvBlob.

Polish + release prep (M8)

  • client.with_scope(project_uid=..., company_uid=...) — returns a copy of the client bound to a different scope, sharing the same authenticated session and connection pool.
  • client.use_project(uid) / client.use_company(uid) — in-place scope rebind.
  • RtlsClient(http_client=...) — caller-supplied httpx.Client for proxies, custom TLS, or other transport tuning.
  • Comprehensive docs/ site built with mkdocs-material (guides, advanced topics, API reference auto-generated via mkdocstrings-python, migration guide).
  • Security audit test (tests/security/test_secret_leaks.py) walking the public surface to verify redaction.
  • LICENSE (MIT). CHANGELOG.md. CI workflow.

Compatibility

  • Python 3.10, 3.11, 3.12.
  • Wheel ships py.typed. Inline type hints work out of the box with mypy --strict and pyright.

Migration

Coming from the JS reference client? Read the migration guide (docs/migration/from-js-client.md) — a code-by-code translation of every common JS idiom into the SDK's equivalent.