Repository navigation
Releases: futureweb/garmin-intervals-bridge
Release list
0.11.3 – swap in place, nothing deleted
Swap in place – the original into Garmin's copy, nothing deleted
Intervals.icu can now put a file into an existing activity – thanks David for building it so quickly. Swap mode uses it.
Added
- Swap in place (
BRIDGE_SWAP_METHOD=replace, the new default): swap mode puts the device's original into Garmin's copy itself (POST /activity/{id}/file).- The activity keeps its id and link, name, description, comments, hand-edited intervals, RPE, gear and tags.
- Nothing is deleted.
- Intervals computes every field and stream from the complete file, and Reprocess file keeps them.
- Intervals still lists the activity as Garmin's import (
source: GARMIN_CONNECT). - Thanks Lukasz_Pludowski for the question about edited intervals.
- The past too:
backfill --scope activities --from DATE --swap --applyswaps older activities in place, with no time limit. - One activity by hand:
swap <activity> --method replace|upload.
Changed
- Swap mode replaces by default. The previous way stays available as
BRIDGE_SWAP_METHOD=upload: the original goes in as a new activity of your own, and Garmin's copy is deleted. You can choose it under How to swap on the web page's settings and in the Home Assistant app. - Charts: a chart the bridge adds (Actions → Charts,
setup-charts) is now a copy of the same chart in Intervals' chart library, the way Intervals' own Copy makes it, so the library shows how many use it. Each copy stays your own; later changes to the library chart don't reach it. - The charts' description links the forum thread instead of GitHub.
setupand the web page say what "log in" means for the background bridge: it starts by itself whenever you log in to this computer (thanks Lukasz_Pludowski).
Fixed
- A Gmail app password works as Google shows it, in groups with spaces, and without them. Before, the spaces made it fail.
- The Gmail hint links Google's app-password page directly; the Security menu shows it only once you have one (thanks Lukasz_Pludowski).
Upgrading
pip install --upgrade garmin-intervals-bridge, pull the new image, or update the Home Assistant app.
- With swap mode on: nothing to change; new activities are now swapped in place. To keep the old upload-and-delete way, set
BRIDGE_SWAP_METHOD=upload. - Activities swapped before stay your uploads.
- Charts you added before stay as they are.
0.11.2 – the Home Assistant app starts again
The Home Assistant app starts again
Fixed: with 0.11.1 the Home Assistant app stayed in "Starting" – the bridge ran fine, but the app could not be stopped or restarted from Home Assistant. 0.11.1 had put the container healthcheck into the image and turned it off for the app with HEALTHCHECK NONE; Home Assistant's Supervisor waits for a health result from any app whose image has a healthcheck entry at all, and with NONE none ever comes. The image carries no healthcheck now. compose.yaml, the Unraid template and the Synology guide set it instead (status --check every 5 minutes, as before). With a plain docker run: --health-cmd "garmin-intervals-bridge status --check" --health-interval 5m. Thanks DarKen.
Upgrading: Home Assistant shows the update. If the app's buttons are still blocked, restart Home Assistant once – the update then goes through. Docker and pip: nothing to do beyond the usual update; Unraid users may re-apply the template to get the healthcheck.
0.11.1 – the web page in Firefox, setup on Windows, a container that checks itself
The web page works in Firefox – and a container that says whether the bridge works
Fixed
- The web page in Firefox: every button and form answered "foreign form". The page asked browsers not to send a referrer, and with that Firefox – as the web's Fetch standard says – sends a form's origin as
null, which the check took for a foreign site. The page now keeps the referrer on its own pages, and anullorigin counts as "no information"; the session's token and cookie still have to fit. Thanks Samantha_Lane and No_Cheetah in the forum. - Windows: a German Windows answered the web page's "Starts at login" check in its console code page, which ended in a
UnicodeDecodeErrorin the console. That's read correctly now. setupon Windows: in the classic PowerShell window Ctrl+V doesn't paste into the hidden API-key prompt but types the control character ^V, so Intervals refused the "key". Whatever a paste brings along (control characters, line breaks, quotes) is dropped now, the prompt says to paste with a right-click, and when too little arrives, it says so. Thanks Okocha in the forum.- A server briefly away:
- A failed check for new activities counts for the pace, so during an outage the bridge tries again at the planned pace (at night every 30 minutes) rather than every minute.
- Under systemd, a connection failure, a timeout or a 5xx is a warning, with no failed unit and no alert mail; a watch that keeps failing is still reported after 3 hours.
- The log names the server and the reason (
intervals.icu: too many 503 error responses).
- Thanks @FrancescoCastaldi for these:
- A setting left empty (
BRIDGE_ACTIVITY_LOOKBACK_DAYS=) means its default instead of stopping the bridge. - A push message's title is always one line.
- A FIT file may not declare more than 32 MB.
- A setting left empty (
Added
- The container image checks itself every 5 minutes (
status --check, no request to anyone). Docker, Portainer, Unraid and Synology show it unhealthy when no run has come for about an hour; a break Intervals asked for, or a Garmin sign-in it's waiting for, count as healthy. The Home Assistant app leaves it out, since Home Assistant watches its apps itself. Thanks @FrancescoCastaldi for the idea. - The whole test suite runs on Windows and macOS too (thanks @FrancescoCastaldi).
- The package declares its type hints (PEP 561
py.typed; thanks @FrancescoCastaldi).
Upgrading
pip install --upgrade garmin-intervals-bridge, pull the new image, or update the Home Assistant app. Nothing to change.
0.11.0 – fewer requests for Intervals.icu, and a second review
Fewer requests for Intervals.icu – and a second review of the whole code
Adaptive polling (beta)
BRIDGE_POLL_MODE=adaptive (in Home Assistant: Adaptive polling; on the web page: Settings) tries to keep the load on Intervals.icu's API as low as it can without making you wait. It learns from your own archive when your activities usually end: per weekday and hour, over the last twelve weeks, with recent weeks counting most. It then checks at your full pace only at those times, every few minutes around them, and every 30 minutes at times you hardly ever upload. For an hour after a new activity it is fast again.
It never checks faster than BRIDGE_POLL_SECONDS, and never outside BRIDGE_POLL_HOURS. Until 15 activities are in the archive, it keeps the full pace. The log and the web page show what it learned. On my own account it comes to about 390 checks a day instead of 1,440.
It is still beta – feedback is very welcome in the forum thread.
Also new
- Quiet hours with a slower pace:
BRIDGE_POLL_QUIET_SECONDS. OutsideBRIDGE_POLL_HOURSthe bridge checks this often instead of not at all; unset, it doesn't check, as before. - Check now on the web page: checks at once, then at the full pace for the next hour.
- The status page shows when the bridge checks and why. The settings page sets the pace, the hours, the quiet pace and the adaptive mode, and so do the Home Assistant app and the Unraid template.
watchdecides itself when a check is due, so the shipped systemd timer starts it every minute again. Awatchthat isn't due ends without a request.- Each FIT file's CRC is checked once instead of three times: about 40 % less CPU per new activity (thanks @Michael.0000 – noticeable on a Raspberry Pi).
A second review
An independent AI code review (OpenAI Codex) went over the whole code base. I checked every finding against the code and reproduced it, then fixed it – about 30 fixes. Most are places where a rare failure could have cost data. None is known to have happened.
- Swap mode:
- Garmin's copy is deleted only once your values read back from the original.
- Comments are checked again right before every delete, retries included.
- An upload whose answer got lost is never sent twice.
- Wellness:
- A read in which one Garmin endpoint failed keeps what the archive had.
- A write to Intervals that failed is tried again.
- Rate limits: a 429 from Intervals pauses the whole run for as long as Intervals asks, and a failing full run backs off instead of retrying every minute.
- Enriching:
- A value you typed in by hand is never overwritten.
- Custom stream scripts are only reproduced when they're the plain copy-one-field form.
- New custom fields are picked up without a restart.
- Web page and settings: request time limits and a connection cap, password checks one at a time, and no setting can be saved that the bridge couldn't start with.
The details are in the CHANGELOG.
Upgrading
pip install --upgrade garmin-intervals-bridge, pull the new image, or update the Home Assistant app.- systemd users: copy the watch timer again. To keep checking every minute:
BRIDGE_POLL_SECONDS=60.
0.10.3 – the Home Assistant app, fixed
The Home Assistant app's first real install, fixed – thanks @darken.
Fixed
- "refused to connect" in the sidebar: the web page forbade being shown inside any frame – Home Assistant's own ingress frame included. Behind ingress, Home Assistant may frame it now (same origin only); everywhere else framing stays forbidden.
- A data folder copied to
share/garmin-bridgeafter the app's first start was never taken over (that start had left an empty database). It is taken over now as long as the app has no Garmin session and no archive of its own – once, and never over them.
Home Assistant offers the update itself; restart the app afterwards.
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.10.3 / futurewebat/garmin-intervals-bridge:0.10.3 (Docker Hub).
0.10.2 – kinder to Intervals.icu: a poll every 5 minutes
Kinder to Intervals.icu: a poll every 5 minutes, and a break when it asks for one.
Upgrading needs no change. To keep polling every minute: BRIDGE_POLL_SECONDS=60 (please only if you really need it). systemd users: the shipped watch timer now runs every 5 minutes; copy it again to take that over.
Changed
- A poll every 5 minutes by default instead of every minute (
run, autostart, containers, the Home Assistant app, the shipped systemd timer) – Intervals.icu asked self-hosted tools not to poll every minute. New activities still arrive within minutes. - A break when Intervals asks for one: an HTTP 429 pauses the polls and full runs for its
Retry-After, at least 15 minutes; the systemd timers skip their runs meanwhile. - The container image's default command is
run(keeps running, a dry run until--apply) instead of a one-time drysync; every template setrunalready.
Documentation
- A current roadmap; the rules contributions keep are in CONTRIBUTING.md.
- The README lists every command, and how to install on Linux or with Homebrew's Python without
--break-system-packages(pipx or a venv).
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.10.2 / futurewebat/garmin-intervals-bridge:0.10.2 (Docker Hub). Home Assistant shows the update itself.
0.10.1 – the bridge as a Home Assistant app
The bridge as a Home Assistant app – thanks @darken.
Added
- Home Assistant app. Add
https://github.com/futureweb/garmin-intervals-bridgeto Home
Assistant's app store (one click),
install Garmin Intervals Bridge, fill in the form – API key, mode (enrich, upload or
backup), Write to Intervals (off = a dry run first), swap, training advice, poll hours,
alerts – and sign in to Garmin (MFA included) on the bridge's web page right in the
sidebar. Home Assistant signs you in; nothing but its ingress proxy can reach the page. The
time zone comes from Home Assistant. Moving in from a NAS: copy its data folder to
share/garmin-bridge, and the app takes it over on its first start – no new Garmin sign-in.
See docs/HOME_ASSISTANT.md. - An app's form (
/data/options.json) now also covers mode, swap, poll hours and interval
and the backup interval.
New: reports from real Home Assistant installs are very welcome.
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.10.1 / futurewebat/garmin-intervals-bridge:0.10.1 (Docker Hub).
0.10.0 – backup mode, a fuller backfill, a calm request counter
Backup mode – your Garmin data in your own folder, no Intervals needed – a fuller wellness
backfill, and a request counter that no longer scares anyone.
Added
- Backup mode (
BRIDGE_MODE=backup,run --mode backup, or answer 2 insetup):
your Garmin Connect data in a folder of yours, kept current – no Intervals account needed,
nothing written anywhere else. Every activity's original FIT with Garmin's summary and
extras (weather, gear, splits, exercise sets); each day's wellness – the full answers of
all ~30 endpoints plus the watch's own wellness files (Health Snapshots included) – once
the day is final and again three days later for a late sync; the account weekly. A run
every 12 hours (BRIDGE_BACKUP_HOURS), about 60 Garmin requests a day.
backup --from 2020-01-01fills in the past, next to a running bridge. Moving to
Intervals later? The archive is exactly what its modes use. See
docs/BACKUP.md.
Changed
- A wellness backfill also reads endurance & hill score and race predictions by default
(15 instead of 12 requests per day;--endpoints essentialfor the old set) – thanks
@jetty. For days backfilled before:
--endpoints endurance_score,hill_score,race_predictions --force-wellness. - Swap mode's wording says plainly that the deleted copy is the one in Intervals – nothing
on Garmin Connect is ever touched (thanks @darken).
Fixed
- The daily summary ("last 24 h … Garmin requests" on the web page and in the health log)
counted far too many Garmin requests when the bridge runs asrun: every minute's check
recorded the whole request count of the last full run again – 50 real requests showed up
as 2,000. Only the count was wrong, not the requests – thanks @Jeroen_Doesburg.
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.10.0 / futurewebat/garmin-intervals-bridge:0.10.0 (Docker Hub).
0.9.1 – poll less, or only in certain hours
Poll less, or only in certain hours – thanks @Michael.0000.
Added
BRIDGE_POLL_SECONDS– seconds between the polls ofrun(default 60, upload mode 600;
--poll-secondsstill wins).BRIDGE_POLL_SECONDS=300polls every five minutes.BRIDGE_POLL_HOURS– poll only in these hours:10-18, several ranges like6-9,16-22,
or over midnight like22-6. Outside themrunskips its polls andwatchdoes nothing;
the half-hourly full runs go on, so an activity recorded outside the window still arrives
(within about two hours instead of a minute) and so does last night's wellness.
The real end of polling would be a webhook from Intervals – see the
feature request.
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.9.1 / futurewebat/garmin-intervals-bridge:0.9.1 (Docker Hub).
0.9.0 – swap mode: Garmin's filtered copy out, the original in
Swap mode: Garmin's filtered copy out, the original in – so Intervals holds the complete file
and "Reprocess file" keeps everything.
Upgrading needs no change; swap mode is off until you switch it on (BRIDGE_SWAP=true, a
question in setup, a switch on the web page).
Added
- Swap mode (
BRIDGE_SWAP=true, off by default). The official Garmin import stays on
and is the trigger: within a minute of Garmin's filtered copy arriving, the bridge
uploads the device's original as your own file, carries over what you may have set
already (name, description, paired workout, type, RPE, feel, tags, gear, commute/race,
ignore settings, custom fields without a script such as the logged intake) and then
deletes the copy Garmin's import created in Intervals – nothing on Garmin Connect is
touched, the bridge only ever reads from Garmin. Intervals computes every custom field and stream from the
complete file itself, Reprocess file keeps them, nothing is marked as edited, and
the activity is your own upload rather than Garmin-sourced data. No polling of Garmin
– one original download per new activity, as before – and with the bridge down the
copy simply stays and is completed or swapped later. - The only place the bridge deletes anything, so it is careful: Garmin's own import
only, withinBRIDGE_SWAP_HOURS(48) of the import, never with comments or
hand-edited intervals on it, not when Garmin sent the unfiltered file anyway; upload
first, delete last – a failure leaves a duplicate to see, never a loss. The new
activity has a new id. swap <id or link> [--apply]: the same for one activity by hand, also an older one.
Changed
enrich <id> --applyand the web page's One activity again also put the logged
intake back (GarminFluidIntake,GarminCaloriesConsumed): Reprocess file empties
every field the file does not fill.- Own streams are rewritten only into the activity they were written to.
Details: README – Modes. Verified live on one activity: all 24 streams and 21 Garmin fields from the original, training load unchanged, and all of it still there after Reprocess file.
Install: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge), or the image ghcr.io/futureweb/garmin-intervals-bridge:0.9.0 / futurewebat/garmin-intervals-bridge:0.9.0 (Docker Hub).