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 onlysrc/,README.md,CHANGELOG.md,LICENSE, andpyproject.toml(previouslyprompts/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
LsbAPIorchestration surface to be driven entirely by saved settings + the active schedule.updateis renamed tosave;start()andstop()are now no-arg and returnbool; newstatus() -> LsbStatus(STARTED/STOPPED/UNKNOWN) compares the nodes currently reporting as master against the configured masters.start()now persistsconfigured_state = Trueandstop()persistsconfigured_state = False. Addedcalc_tdoas(schedule=None)(parses the active v2 schedule →max(slot)+1) andsend_schedule(schedule=None)(pushes a per-anchortdoa_scheduleplist). Removedstart_configured, the arg-takingstart(masters)/stop(macs), andLsbMasterSpec;start/stopno longer raiseLsbConvergenceError(they returnFalseon timeout). v2 (TDOA) only. - Breaking: renamed the trackable-association surface from
AssociationtoTagAssociationto disambiguate it fromAnchorAssociation. The resource classAssociationsAPIis nowTagAssociationsAPI, the client attributeclient.associationsis nowclient.tag_associations, and theSessionContext.associationsfield (and itserrorsdict key) is nowtag_associations. Module filesmodels/association.py/resources/associations.pywere renamed tomodels/tag_association.py/resources/tag_associations.py. Server wire paths, envelope keys, and theAnchorAssociationsurface are unchanged. - Breaking:
TagAssociationsAPImethod arguments renamed for brevity/consistency:mac_address→macandtrackable_uid→obj_uidacrosscreate,open,close(andbulk_createtuples). The wire body keys (mac_address,obj_uid) and theTagAssociationmodel fields are unchanged. - Breaking: reworked
LsbSettings(LSB/TDOA v2 only). The config nested undervaluesis lifted onto typed top-level members on read —active_schedule_uid,autostart,configured_state, and themasters/distributorslists (parsed into the new nestedLsbSettings.Master/LsbSettings.Distributormodels) — and the rawvaluesblob is dropped. These members are the single source of truth:lsb.update(settings)now takes anLsbSettingsand rebuildsvaluesfrom them. Themasters()/distributors()accessor methods, theLsbMasterEntry/LsbDistributorEntrydataclasses, the v1autostart_devicesderivation, and the oldupdate(project_uid, *, values=...)signature were removed. The model modulemodels/lsb.pywas renamed tomodels/lsb_settings.py.LsbSettings(and its nestedMaster/Distributor) are mutable — unlike the SDK's other (frozen) models — to support the get → edit →updateworkflow.
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); mirrorsAnchorAssociationsAPI.open.
[0.5.0] — 2026-05-19¶
Added¶
client.anchor_associations— node ↔ placed-anchor binding on the/api/v2/win_anchor_associationsresource:list,list_all(sends?all=true),create(mac_address, anchor_uid),update(uid, *, mac_address=..., anchor_uid=...),close(...)(byAnchorAssociation, uid, or kwargs),delete(uid). The server's9999-12-31T23:59:59.000Z"open" sentinel is mapped toclosed_at=Noneon the newAnchorAssociationmodel. The wire field iswin_uid; the SDK exposes it asanchor_uidon method signatures. Server PUT requires bothmac_addressandwin_uid; partialupdate()calls do an implicitlist_all()lookup to fill the unchanged field.- Unit tests in
tests/unit/test_resources_write_m3.py(TestAnchorAssociationsWrites) and live tests intests/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 opaquecontentblob so callers can round-trip both v1 and v2 schedule wire formats without an SDK release. Newrtls_sdk.Schedulemodel.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 byproject_uidin the body. Thevaluespayload is exposed asdict[str, Any]to support bothlsb_version=1(sfd,autostart_devices) andlsb_version=2(masters,distributors) shapes side-by-side. The SDK also merges the top-leveltdoa_version/lsb_versionfields from the response into the model. Newrtls_sdk.LsbSettingsmodel.client.network—/api/v2/network_settings/read/update for the per-project service-mode toggle (enabled,duration,devices_in_service_mode). Newrtls_sdk.NetworkSettingsandrtls_sdk.ServiceModemodels; 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 byRTLS_LIVE_ALLOW_SERVICE_MODE_TOGGLE=1because 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)— pushsync_slot+sfto each master and pollhw_paramsuntil every node echoes back the configured values. RaisesLsbConvergenceError(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-zerosf"stop trick" to each master and poll until every node reportssf.value.sfd == 0(oraddrempty /"0000").- New
LsbMasterSpecdataclass (mac,addr,slot,sfd) for thestart()argument. - New
LsbConvergenceErrorexception carryingunresolved: list[str].
LSB settings accessors and configured-start convenience¶
LsbSettings.masters()andLsbSettings.distributors()— typed views over the opaquevaluesdict that smooth the v1/v2 fork. Return frozenLsbMasterEntry/LsbDistributorEntrydataclasses (also exported at the top level). The wire shape ofvaluesstays 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 (fromnode.node_id), then delegates toclient.lsb.start([...]). RaisesRuntimeErrorwith a clear message when resolution fails (no masters configured, no registered node for an anchor's MAC, etc.).bcsdefaults tomax(slot) + 1matching 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.jsonwith repeatedmac=andfields=query parameters (matching the desktop client's wire format). Returnslist[NodeParams]. Server-side"pending"strings on each field coerce toNoneso callers always seedict | None.- New
rtls_sdk.NodeParamsmodel.
[0.3.0] — 2026-05-13¶
Documentation¶
- New
entities/streaming.mddocumentingclient.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 fromWsChannelEnvelopeand carry the full wire dict on.rawfor forward-compat field access.client.ws.subscribe(...)now yields typed messages instead of the M17 untyped envelope; iteration patterns likematch msg: case PositionMessage(x=x, y=y): ...work directly. The yield type narrowed fromWsChannelMessagetoWsChannelEnvelope.- 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 = Falsekwarg onclient.ws.subscribe(...). By default,notifyframes withaction == "HB"are dropped at the reader; opt-in callers receive them asNotifyMessage(action="HB"). Heartbeat drops do not count towardsession.messages_dropped— that counter remains backpressure-only.
Changed¶
WsChannelMessagemoved fromrtls_sdk._wstortls_sdk.models.ws; the import path under the top-level package (from rtls_sdk import WsChannelMessage) is unchanged.
WebSocket subscription scaffold (M17)¶
client.ws— newWsAPIsub-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 ofWsChannelMessageenvelopes. Naming is transport-specific on purpose — future streaming protocols (MQTT, Kafka) would land as siblings (client.mqtt,client.kafka) rather than crowd a genericclient.streamnamespace.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 asWsAuthErrorfrom the iterator and stops further attempts. - New error classes
WsError,WsAuthError, andWsProtocolError(all subclasses ofRtlsError). - New dependency:
websockets >= 12.0.
User messages REST (M16)¶
client.user_messages.send(tag_uid, hex)— newUserMessagesAPIsub-client; sends a hex payload to a tag's badge over the WIN gateway. Wire endpoint:POST /api/v2/user_msg/. Independent ofclient.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 wirelocationsresource.updateuses a conservativename/shiftallowlist mirroring the JS reference client;createaccepts the wider server payload via**fields. Server-side cascade ondelete(areas / floorplans / anchors / zones go too).client.areas.create / update / delete— wiresublocationsresource.createrequiresname+location_uid;updatepasses arbitrary placement fields (position_x,position_y,width,height,rotation) through. Server-side cascade.client.anchors.create / update / delete— wirewin_anchors.createenforcesname,position,type(ANCHORorBRIDGE, validated locally); optionallocked,acc_config,bt_config,state,device_configride 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'smac_regexrequires. Wired throughnodes.create / validate / assign_raw / release_raw / release / import_andassociations.create / bulk_create.- Live fixtures
writable_site_uid/writable_area_uidfor destructive live tests against the shared test server, withsdk-test-<uuid4>prefixes andyield-style cleanup.
Floorplans + image lifecycle (M10)¶
client.floorplans.create— base64-in-JSON image upload. Image is required; passimage_bytes+original_filenameORimage_path(either-or, enforced withValueErrorbefore any HTTP call).client.floorplans.update— diff-driven; replaces the stored image only whenimage_bytes/image_pathis 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; optionalstream_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.createno longer acceptsmac_address. The server's JoicreateTrackObjectschema has nomac_addressfield; the defaultstripUnknownsilently dropped it, so the call appeared to succeed but the MAC never bound. Callers who want hardware binding now go throughnodes.create+associations.create(the migration guide anddocs/entities/tags.mddocument the three-call replacement).- Canonical MAC format is bare 12-hex lowercase. The server's
mac_regexrejects 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.createnow includes aroleon the seeded admin user (defaults to"company_admin"; override viaadmin_role=). The JoicompanyCreate.user.rolefield is required; previously the SDK omitted it and the server responded"user.role" is required.projects.create/projects.updatenow wrap the body under{"project": {...}}to match the JoicreateProject/updateProjectschemas. The previously-flat body was rejected with"project" is required.associations.closenow sendsmac_address/obj_uidinstead of the association's ownuid. 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 anAssociationobject, an association uid (with an extralist()lookup), or explicitmac_address/trackable_uidkwargs.compounds/nodes.releaseupdated to pass theAssociationobject 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.mdto 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 fromRTLS_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_sdknamed 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_rawfor 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_rawand 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 returningBulkResult).tags.bulk_update(uids, changes).zones.create/update/deletewith dynamic-binding management.groups.create/update/deletewith membership cleanup.associations.bulk_createcontinue-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 withpartial/errorsfan-out tracking.
Session context (M6)¶
client.context.load()— the dashboard bootstrap. Fans out ~20 sub-resource reads into oneSessionContextsnapshot withpartial/errorspartial-failure semantics.- Cross-scope nodes fetch via internal per-thread scope override.
Reports + pagination (M7)¶
client.reports.heatmap(...)— POST + poll until ready, withReportFailed/ReportTimeoutoutcomes.client.reports.pws(...)— aggregate PWS event report.client.reports.zone_activity_csv(...)— chunked CSV download followingcontent-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-suppliedhttpx.Clientfor proxies, custom TLS, or other transport tuning.- Comprehensive
docs/site built withmkdocs-material(guides, advanced topics, API reference auto-generated viamkdocstrings-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 withmypy --strictandpyright.
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.