Changelog
spdgt.telem 0.4.10
The package now declares a telemetry address for each region, so it works in Canada again.
.onLoad()registered a single unnamed URL, which spdgt.auth 2.0.0 accepts only in the"us"region — and only with a warning. Anywhere else it is an error, because the address for that region is genuinely unknown:api_url("telemetry")underSPDGT_REGION=caaborted with “has no address for region”, taking every read and write with it.usresolves tomarks.spdgt.comandcatomarks.ca.spdgt.com, and spdgt.auth picks between them when a URL is read rather than when it is registered, so setting the region before or afterlibrary(spdgt.telem)works either way.Requires spdgt.auth 2.0.0. The named-address form does not exist before it.
Worth knowing for anyone auditing the rest of the estate: a package registering
"counts"with the old single-URL form is unaffected, because spdgt.auth registers that API itself with both regions and declines the stale re-registration. Only the APIs spdgt.auth does not own — telemetry here, and the Cloud Run services elsewhere — actually break.
spdgt.telem 0.4.9
No user-facing change. The documentation site was left a version behind: 0.4.6, 0.4.7 and 0.4.8 were merged within half a minute of each other, their builds raced to force-push the same branch, and the site that survived was 0.4.7’s. Nexus held all three throughout. This release exists so the site rebuilds, and records the trap in
CLAUDE.md— re-running a build cannot fix it, because the Nexus upload runs first and refuses an already-published version before the documentation step is reached.CLAUDE.mdno longer lists which versions the public Nexus holds. It went stale four times in three weeks, two lines above the query that answers the question properly.
spdgt.telem 0.4.8
Every
telem_update_*()now validates before sending and refuses to write from a session with no project, as the create functions already did. An update previously went out unchecked: a misspelled enum or an over-long string came back as a 422 from the server rather than failing here, and four of the eleven did not even confirm anidwas present.telem_validate()gains anactionargument.action = "update"relaxes the required fields to optional, because an update sends only what it is changing — the API says as much itself, marking every fieldsometimeson its update requests where the create request marks itrequired. Apresentfield stays present, sostateis still expected on a device update. Types, limits and enumerations are enforced identically either way, andidis required on an update and never reported as an unknown column.Two invalid values in the package’s own tests were caught by this and corrected: a capture method of
"Darting", which is not one, and an event type of"Drop-off", which is the right event in the wrong case.
spdgt.telem 0.4.7
telem_create_positions()now checks that the payload actually yields a fix before sending. Positions are the one resource whose required values live inside a JSON payload rather than in columns, so the schema could only insist the column was there. A payload carrying temperature and voltage but no coordinates passed validation and was accepted by the API, which then marked every eventFailed— stored, counted, and left for someone to find.The check asks exactly what the server asks. Coordinates are resolved case-insensitively through the same alias list
ProcessGPSuses (latitude_dd,latitude,lat_dd,lat,y, and the longitude equivalents), so a raw vendor record passes untouched and only a payload the server could not read is refused. A pair of exactly0, 0is refused for the same reason it is: the server treats it as no fix at all.Coordinates outside their range are refused too. The server does not reject one, it stores it, so a transposed latitude and longitude becomes a position in the wrong hemisphere rather than an error.
spdgt.telem 0.4.6
A create with no rows is now refused. An empty frame reached the API as an empty batch, which some endpoints answered with an error and positions reported as a success having sent nothing — the silent success this package exists to prevent, arriving through a filter that matched nothing.
Date columns are now rendered for the wire like datetimes already were. A
DateorPOSIXctinbirth_date,death_date,purchased_at,retired_atordate_of_capturewent out in whatever shape the JSON encoder chose, while the same value instart_atwas normalised. Both are normalised now, dates toYYYY-MM-DD.Supplying
project_idfrom the session now fails loudly when there is no project, instead of returning the data unchanged. Every caller runscheck_project()first, so this cannot be reached today — but returning unchanged would have sent a record the API stores where nothing can read, update or delete it, and that is not a failure worth making quietly.The confirmation prompt for incomplete deployments is now covered by tests. Reaching it needs an interactive session, so the check uses
rlang::is_interactive()rather thanbase::interactive(), which a test can declare and a base binding cannot.
spdgt.telem 0.4.5
alberta_assign_units.Rnow names the country it works on, like the two load scripts already did. It was the only one of the three without that guard, and the only one that touches both APIs – it reads unit polygons from counts and writes the ids onto telemetry records – so it checks both resolve to the same country before doing anything. Run in a session pointed somewhere else, it would have written one country’s unit assignments onto another’s records, which deleting rows afterwards does not undo.The same script no longer says you will be asked to sign in twice. One sign-in serves both APIs: telemetry is served from the counts credential.
spdgt.telem 0.4.4
- Bug reports now go to https://support.spdgt.com, and
DESCRIPTIONcarries a realBugReportsfield for the first time. The field was previously absent because the address was an email, whichR CMD checkrejects as amailto:, and a link to the private source repository would have sent readers somewhere they cannot reach — so the documentation site’s sidebar link was written by hand instead. A support URL removes both objections, so the workaround is gone and the link is generated from the package metadata like any other.
spdgt.telem 0.4.3
-
DESCRIPTIONnow requiresspdgt.auth (>= 1.2.1). Everytelem_update_*()passes its single-row frame straight toapi_patch(), and before 1.2.1 a base data frame was sent as a one-element array rather than an object — which the API accepts with a success response while writing nothing. Installed against the old floor of 1.1.0.19, the update functions returned success and changed nothing. The floor is 1.2.1 because that is the version carrying the fix, not the newest available.
spdgt.telem 0.4.2
- No user-facing change. The note recording which versions the public Nexus holds is brought up to date, and the release exists so the documentation site publishes: the pkgdown output has been building correctly for some time but nothing was serving it, and the Vercel project that now does deploys from the
gh-pagesbranch this build refreshes.
spdgt.telem 0.4.1
The reference index lists every exported function again. The biological records added since 0.3.1 – blueprints, sensor deployments, groups, group individuals, diagnoses, procedures and samples – were exported without being added to
_pkgdown.yml, and pkgdown treats an unlisted topic as an error, so the documentation site had been building only partially.The Alberta load scripts in
inst/scripts/now select the country’s database explicitly withSPDGT_REGIONand refuse to run if the telemetry API does not resolve to the expected regional host. Alberta data is Canadian; writing it to the United States instance is the one mistake that deleting rows afterwards does not undo, so a region that did not take effect is an error rather than a warning. The scripts also requirespdgt.auth (>= 1.1.0.20), the version that introduced regions, because an older one would resolve silently to the wrong country.
spdgt.telem 0.4.0
Every
telem_create_*()now validates its data against the fields the API actually accepts before sending anything, and reports every problem at once rather than stopping at the first.telem_fields()is that list, andtelem_validate()runs the check on its own if you want it earlier. The fields come from the API’s own validation rules;spdgt.auth::api_discover()reports some of them wrongly, because it serves an OpenAPI annotation that has drifted from the rules beneath it.The vendor collar layer is removed:
read_collar(),default_col_map(),prep_individuals(),prep_devices(),prep_deployments(),prep_positions()andtelem_ingest(). It targeted one export shape and degraded silently on anything else — a column mapping it could not resolve was dropped rather than raised, yielding all-NAcolumns that passed every check downstream. Reshaping arbitrary vendor files was never a job this package could finish; guaranteeing what reaches the API is.spdgt.telemis pre-1.0, so this is a clean break with no deprecated aliases.New
telem_add_payload()builds thepayloadcolumn a position upload needs, keeping every column the collar recorded rather than the four the oldprep_positions()kept. This matches what the server itself stores.telem_create_positions()takes that column directly.Writes are refused when the session carries no project. Such a write is accepted by the API and reported as a success, but the resulting row cannot be read, updated or deleted by anyone — a previous load left 21 device rows that had to be removed in SQL.
telem_create_deployments()stops and asks when a deployment has noindividual_idor nostart_at. The API accepts these and records them asIncomplete, which cannot anchor a position and is never reported back. Passon_incomplete = "allow"for a collar not yet on an animal.telem_create_positions()now warns when the data carries adeployment_id, astelem_create_events()already did. Positions post to the same endpoint, so the API discards the column the same way while reporting success; it was previously removed before the request was built, without a word. The column is sent regardless, so this starts working with no change here once the API accepts it.telem_validate()reports an unrecognised enumerated value readably, and in the same words as the rest of the package. It previously printed the deparsed vector ("\"Bogus\""), and a value containing a brace aborted with a cli error instead of the validation problem.An unfiltered read now warns when it is about to return more records than
max_records(50,000 by default). Reads still return every record by default; the event tables are simply large enough that the whole of one is rarely what anybody meant.telem_event_sources()gains"CSV", which is in wide use. Note that"API"means the record was fetched from the manufacturer’s API by the server, not that a client wrote it.vignette("loading-data")is rewritten around the schema: the order records have to be created in and the two traps that order alone does not reveal, whattelem_fields()says and how to read it, how to check a batch before sending it, and how to upload positions you hold locally. Its hand-maintained tables of required columns are gone — they had already drifted from the API, andtelem_fields()cannot.DESCRIPTIONno longer pinsdplyr (>= 1.2.0). The pin existed forrecode_values(), used by the deletedmap_sex(); the only dplyr function left in the package isbind_rows(), so requiring a 2026 dplyr asked consumers to upgrade for nothing.
spdgt.telem 0.3.1
- Every read function’s help now links to [spdgt.auth::api_discover()] as a real cross-reference rather than naming it in backticks, and
DESCRIPTIONrequiresspdgt.auth (>= 1.1.0.19)— the version that added it. Both were deferred in 0.2.0 for a reason that has since gone away: a cross-package Rd link only resolves once that version is installable, andapi_discover()was merged but unpublished. It is on Nexus now.
spdgt.telem 0.3.0
The vendor collar-file layer moved here from
spdgt.telem.admin:read_collar(),default_col_map(),prep_devices(),prep_individuals(),prep_deployments(),prep_positions()andtelem_ingest(). Nothing in them is privileged — parsing a vendor CSV needs no role — so an admin-tier package was the wrong home, and with the CRUD already here they had been left calling across a package boundary for no reason.spdgt.telem.adminkeeps its repo for genuinely admin-only work later, and exports nothing for now.vignette("loading-data")gained the collar-file section it previously only pointed at: whatread_collar()normalises, thecol_mapfallback that yields all-NAcolumns rather than an error when a mapping cannot be resolved, and theprep_*()sequence with the lookups that bridge it.inst/scripts/quinault_elk_load.Ris a worked per-project loading script, readable start to finish. It existed only on an unmerged branch of the admin package and would have been lost.
spdgt.telem 0.2.0
The package now writes as well as reads. Create, update and delete functions for individuals, devices, deployments, captures, events and positions moved here from
spdgt.telem.admin, which is where they were written and the wrong place for them: nothing in them is admin-only, and end users need programmatic access to the same operations. What a caller may actually do is decided by the API from their role, not by which R package they installed — creates and updates need a project relationship, and deletes need the Admin role, so aUser-role caller gets a 403 rather than a deletion.The four
telem_read_*_id()functions are removed, replaced bytelem_read_individuals(),telem_read_devices(),telem_read_deployments()andtelem_read_positions(). The_idsuffix was a misnomer — none of them read a single record by id; they read a filtered list, which is what the replacements do. The replacements also drop theproject_idargument, which the API fills in from the caller’s token, and addpages.spdgt.telemis pre-1.0, so this is a clean break with no deprecated aliases.Reads now return every matching record by default. The API paginates at 100, so the previous behaviour silently returned the first page — on the Quinault device list that was 100 of 179 records, with nothing to indicate the rest existed. Pass
pages = list(size = 100)to page deliberately.New
telem_read_captures()andtelem_read_events().build_lookup()moved here alongside the create functions, because they are barely usable without it. A batch endpoint answers with one row per submitted record and the created record inside adatalist-column, so the assigned ids are not where a caller reaches for them;build_lookup()flattens that to the successful records and returns the named vector that turns a serial number or nickname into thedevice_idorindividual_idthe next call needs.New enumeration functions expose the values the API accepts, so a payload can be built and checked before it is sent rather than coming back as a 422:
telem_event_types(),telem_event_statuses(),telem_event_sources(),telem_device_states(),telem_capture_methods(),telem_deployment_statuses(),telem_fates(),telem_sexes()andtelem_device_manufacturers(). They mirrorapp/Enums/*.phpintelemetry-api, which stays authoritative. The create functions validate against them locally.Read functions now name the filters people actually reach for, so nobody has to know a filter name to get started:
species_id,sex,fateandnicknameon individuals;serial_numberandstateon devices;methodon captures;deployment_idandstatuson events; andfrom/toon deployments, captures, events and positions. Anything else the endpoint accepts still goes through..., and every read function’s help now points atspdgt.auth::api_discover()for finding out what that is rather than guessing.Bug fix:
telem_read_positions(date_range = )never worked and is replaced byfromandto. The API’s filter vocabulary has no two-sided comparison: a comma-separated value is awhereInlist, sodate_range = "2026-01-01,2026-06-30"asked for positions at exactly those two instants and returned nothing rather than the span between them. Prefixing does not rescue it either —">a,<b"strips the prefix from the first element only and then applies that one operator to every element, comparing against the literal string"<b". Sofrom/tosend one bound to the server and apply the other locally, which is correct at the cost of more rows travelling than were asked for. Both bounds are inclusive, and a plain date astomeans the end of that day.Filter coverage is uneven across endpoints in ways worth stating, since guessing wastes time: species exists only on individuals, spatial filtering only on positions (
bounds), and time on every endpoint but under a different column each — which is whyfrom/tomap to whichever column that endpoint means.New
telem_mortality_causes()gives a shared vocabulary for cause of death. The API has no field for it, and none for predator species either, socauseandpredatorcolumns passed totelem_create_events()are lower-cased and folded intonoteswith a message saying so.causeis checked against the vocabulary and warns on anything outside it;predatoris free text, since a predator list is a species list and this package is not the authority on it. Source data commonly conflates the two — a cause recorded literally as"cougar"— and splitting it intocause = "predation",predator = "cougar"keeps both facts without a cause vocabulary per predator. A predator left incausestill reachesnotes, after a warning: recording it imprecisely beats losing it. This is a labelled workaround, not a schema.telem_create_events()now warns whendeployment_idis supplied. The column is in the Event model’sfillablebut absent from the create request’s validation rules, so the API discards it and still reports success — which is how 130 mortality events ended up linked to no animal and needed repairing in SQL. The value is sent anyway, so this starts working with no change here once the API accepts it.Workarounds for known API gaps are collected in
R/api_gaps.R, each recording what to delete when the API side lands, so they can be removed together rather than found one at a time.Two vignettes:
vignette("spdgt-telem")covers reading, discovery and what your role allows, andvignette("loading-data")is the write reference — the order records have to be created in, what each endpoint requires, and how to get the assigned ids back out of a batch result.Live debug scripts in
inst/debug/, for exercising the endpoints against the real API where the mocked test suite cannot.write.Ris a dry run unlessTELEM_DEBUG_LIVE=yes, and its live section sits inside anif (live) { }block rather than behind an early return:return()at file scope does not stop a sourced script, so a return-guard would print “dry run only” and then write anyway.Repo hygiene:
.vscode/is now gitignored. The JS worm spread through atasks.jsonfolderOpentask executing a payload disguised as a font — the vendored guard catches the payload, and ignoring the folder removes the vector.The package’s documentation URL is now
https://docs.spdgt.com/telem, matchingspdgt.coreandspdgt.sight; it had pointed at agithub.ioaddress that is not where the estate serves docs.DESCRIPTIONgained the matchingURLfield it was missing.Bug reports go to software@speedgoat.io, shown in the README and in the pkgdown sidebar. There is deliberately no
BugReportsfield: this package is installable by anyone from the SpeedGoat repository while its source repo is private, so a GitHub issues URL points somewhere the people installing it cannot reach, andR CMD checkrejects amailto:there as “not a suitable URL”. The sidebar link is therefore configured by hand rather than derived fromDESCRIPTION.
spdgt.telem 0.1.0
- Initial release with ID-based read functions.
-
telem_read_individuals_id()to query individuals by project or species. -
telem_read_deployments_id()to query deployments by project, device, or individual. -
telem_read_devices_id()to query devices by project. -
telem_read_positions_id()to query GPS positions with date range and spatial filters.