intervals-summits 0.1.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.
Files changed (26) hide show
  1. intervals_summits-0.1.0/LICENSE +21 -0
  2. intervals_summits-0.1.0/PKG-INFO +223 -0
  3. intervals_summits-0.1.0/README.md +204 -0
  4. intervals_summits-0.1.0/pyproject.toml +34 -0
  5. intervals_summits-0.1.0/setup.cfg +4 -0
  6. intervals_summits-0.1.0/src/intervals_summits/__init__.py +2 -0
  7. intervals_summits-0.1.0/src/intervals_summits/__main__.py +5 -0
  8. intervals_summits-0.1.0/src/intervals_summits/cli.py +266 -0
  9. intervals_summits-0.1.0/src/intervals_summits/config.py +142 -0
  10. intervals_summits-0.1.0/src/intervals_summits/fields.py +156 -0
  11. intervals_summits-0.1.0/src/intervals_summits/intervals.py +111 -0
  12. intervals_summits-0.1.0/src/intervals_summits/match.py +63 -0
  13. intervals_summits-0.1.0/src/intervals_summits/osm.py +160 -0
  14. intervals_summits-0.1.0/src/intervals_summits/page.py +164 -0
  15. intervals_summits-0.1.0/src/intervals_summits/store.py +163 -0
  16. intervals_summits-0.1.0/src/intervals_summits/sync.py +239 -0
  17. intervals_summits-0.1.0/src/intervals_summits.egg-info/PKG-INFO +223 -0
  18. intervals_summits-0.1.0/src/intervals_summits.egg-info/SOURCES.txt +24 -0
  19. intervals_summits-0.1.0/src/intervals_summits.egg-info/dependency_links.txt +1 -0
  20. intervals_summits-0.1.0/src/intervals_summits.egg-info/entry_points.txt +2 -0
  21. intervals_summits-0.1.0/src/intervals_summits.egg-info/requires.txt +4 -0
  22. intervals_summits-0.1.0/src/intervals_summits.egg-info/top_level.txt +1 -0
  23. intervals_summits-0.1.0/tests/test_cli.py +76 -0
  24. intervals_summits-0.1.0/tests/test_fields.py +85 -0
  25. intervals_summits-0.1.0/tests/test_match_osm.py +62 -0
  26. intervals_summits-0.1.0/tests/test_sync_page.py +194 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Schnederle-Wagner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: intervals-summits
3
+ Version: 0.1.0
4
+ Summary: Summits, huts and passes on your Intervals.icu activities, from your GPS tracks and OpenStreetMap – with a summit log page
5
+ Author: Andreas Schnederle-Wagner
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/futureweb/intervals-summits
8
+ Keywords: intervals.icu,summits,peaks,openstreetmap,hiking,ski touring
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: GIS
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.31
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # intervals-summits
21
+
22
+ [![CI](https://github.com/futureweb/intervals-summits/actions/workflows/tests.yml/badge.svg)](https://github.com/futureweb/intervals-summits/actions/workflows/tests.yml)
23
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
24
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)
25
+ [![Garmin Intervals Bridge](https://img.shields.io/badge/companion-Garmin%20Intervals%20Bridge-6f42c1)](https://github.com/futureweb/garmin-intervals-bridge)
26
+ [![Futureweb Intervals MCP](https://img.shields.io/badge/companion-Futureweb%20Intervals%20MCP-0a7bbb)](https://github.com/futureweb/intervals-mcp-server)
27
+
28
+ **Summits, huts and passes on your [Intervals.icu](https://intervals.icu) activities**, found in your
29
+ own GPS tracks with OpenStreetMap, plus your personal **summit log**.
30
+
31
+ ![The summit log page (demo data)](docs/images/summit-log.png)
32
+
33
+ - **In Intervals:** every activity gets its summits in a field, e.g. `Maukspitze (2231 m), Ackerlspitze (2329 m)`.
34
+ Two more fields: the number of summits, which you can chart per week, month or year, and the
35
+ highest point of the activity.
36
+ - **The summit log page:**
37
+ - your summits, huts and passes over time;
38
+ - your top 10/20/50/100 (most visited or highest);
39
+ - a count per year;
40
+ - a searchable log, every entry linked to its activity in Intervals.
41
+ - **It works with every activity that has a GPS track**, whatever it came from: Garmin, Wahoo, Coros,
42
+ Suunto, Polar, a Strava import or an upload. Old ones too: one backfill matches your whole history.
43
+ - It needs nothing but your Intervals API key.
44
+
45
+ **Sister projects** (each works on its own, together they do more):
46
+
47
+ - [**Garmin Intervals Bridge**](https://github.com/futureweb/garmin-intervals-bridge) puts back what
48
+ Garmin strips from the FIT files it sends to Intervals.icu and syncs the wellness data the official
49
+ sync misses. intervals-summits runs next to it with the same settings, and the bridge tells it when
50
+ there is something new ([how](#next-to-garmin-intervals-bridge-the-bridge-says-when-there-is-something-new)).
51
+ - [**Futureweb Intervals MCP**](https://github.com/futureweb/intervals-mcp-server) lets ChatGPT, Claude
52
+ and any MCP client read and analyse your Intervals data. It reads the `Summits` fields like any other,
53
+ so you can ask your assistant which summits you climbed this year, or how often.
54
+
55
+ ## How a summit is found
56
+
57
+ - **Peaks** (`natural=peak`/`volcano`): the track comes within **100 m**, *and* the track's altitude
58
+ there is at most **80 m below** the summit. A path along the slope below a summit does not count;
59
+ a gondola ride to the top does.
60
+ - **Huts** (`tourism=alpine_hut`/`wilderness_hut`) and **passes** (`mountain_pass=yes`,
61
+ `natural=saddle`): within **60 m**.
62
+ - One entry per name and activity, in the order of the track.
63
+
64
+ These rules were calibrated against peakproject's summit lines on 25 activities: 38 of 40 summits
65
+ were found. A summit that OpenStreetMap does not have, or has under another name, cannot be found.
66
+ Improve the map, and `recompute` picks it up.
67
+
68
+ **Stricter or more generous:** the four numbers can be set (`SUMMITS_PEAK_RADIUS`,
69
+ `SUMMITS_PEAK_BELOW`, `SUMMITS_HUT_RADIUS`, `SUMMITS_PASS_RADIUS`, see [Settings](#settings)). For example
70
+ `SUMMITS_PEAK_RADIUS=50` and `SUMMITS_PEAK_BELOW=30` count only summits you were really on. Best set
71
+ them before the first backfill. Changed later, they apply to new activities at once, and
72
+ `recompute --from 2010-01-01` applies them to your past (from the stored tracks, without downloading them
73
+ again). An activity left with no summit at all keeps the values written before; clear those by hand.
74
+
75
+ ## What lands in Intervals
76
+
77
+ ![The fields on an activity page in Intervals](docs/images/intervals-activity.png)
78
+
79
+ | Field (code) | Type | Content |
80
+ |---|---|---|
81
+ | `Summits` | text | `Maukspitze (2231 m), Ackerlspitze (2329 m)`: the summits in the order you reached them |
82
+ | `SummitCount` | number, summed per day | how many summits; chart them per week, month or year |
83
+ | `HighestPoint` | number, max per day | **only where empty.** Intervals fills it from a FIT file itself; for GPX/TCX and old activities the tool takes the highest point of the track |
84
+ | `HutsPasses` | text | huts and passes. Off by default: `SUMMITS_FIELDS=…,HutsPasses` |
85
+ | description | one line | optional, `SUMMITS_DESCRIPTION=true`: `⛰️ Summits: Maukspitze (2231 m), …`, **so Intervals' search finds them** (see below) |
86
+
87
+ The tool creates missing fields itself.
88
+ - **Codes are the same for everyone**, so charts and scripts can be shared.
89
+ - **Names** are English, or German with `SUMMITS_LANGUAGE=de`. You may rename them in Intervals freely:
90
+ the tool goes by the code.
91
+
92
+ **Your edits stay.** A value you typed, or another tool wrote, is never overwritten (unless
93
+ `--force`). The tool only updates what it wrote itself. In the description it only ever touches its
94
+ own line: it reads the description again right before writing, adds the line at the end after a blank
95
+ line, and leaves every other character as it is. If the description already names all the summits
96
+ (peakproject's lines, for example), it adds nothing.
97
+
98
+ ### Seeing them in Intervals
99
+
100
+ - **On the activity:** the fields show above the charts. Which fields show is chosen per sport, and
101
+ they can go into the summary layout.
102
+ - **Filters:** in the activity list and on the totals page, filters can use custom fields, e.g. `SummitCount` at least 1 for all your summit days.
103
+ - **Fitness charts:** `SummitCount` (summed per day) and `HighestPoint` (maximum per day) can be
104
+ plotted; on a weekly or monthly scale you get summits per week or month.
105
+ - **Search:** Intervals' search looks at names and descriptions, not at custom fields. Turn on
106
+ `SUMMITS_DESCRIPTION=true`, and searching a summit finds every activity that reached it, also
107
+ where it is not in the name:
108
+
109
+ ![Intervals' search for "Schatterberg": tours that have it in no name](docs/images/intervals-search.png)
110
+
111
+ ## Installing and running
112
+
113
+ ### Docker (NAS, server, next to the bridge)
114
+
115
+ ```bash
116
+ curl -O https://raw.githubusercontent.com/futureweb/intervals-summits/main/compose.yaml
117
+ curl -o .env https://raw.githubusercontent.com/futureweb/intervals-summits/main/.env.example # put your key in
118
+ docker compose up -d # a dry run: the summit log fills, nothing is written
119
+ docker compose logs -f # what it would write
120
+ docker compose run --rm summits backfill --from 2015-01-01 --apply # your past
121
+ # then command: ["run", "--apply", "--ui", "--host", "0.0.0.0"] in compose.yaml and up -d again
122
+ ```
123
+
124
+ The summit log is at **http://localhost:8766/**. Next to the bridge, add the service to the bridge's
125
+ `compose.yaml` and use its `.env`: the same key works.
126
+
127
+ ### Home Assistant
128
+
129
+ Add `https://github.com/futureweb/intervals-summits` as an app repository (Settings → Apps → App store
130
+ → ⋮ → Repositories). Then install **Intervals Summits**, enter your Intervals API key, optionally
131
+ "Backfill from", and start it. The summit log appears in the sidebar. Switch on *Write to Intervals*
132
+ once the dry run looks right.
133
+
134
+ ### On your computer (Windows, Mac, Linux), Python 3.12+
135
+
136
+ ```bash
137
+ pip install intervals-summits # Windows: py -m pip install intervals-summits
138
+ intervals-summits backfill --from 2015-01-01 # dry run over your past
139
+ intervals-summits backfill --from 2015-01-01 --apply # … and write
140
+ intervals-summits run --apply --ui # keep running; the summit log on http://127.0.0.1:8766/
141
+ ```
142
+
143
+ Settings come from the environment, else from `./.env`, `~/IntervalsSummits/.env` or the bridge's
144
+ `~/GarminBridge/.env`. A bridge set up on the same computer is enough: nothing to configure twice.
145
+ Instead of keeping it running, `intervals-summits sync --apply` once a day (Task Scheduler, cron)
146
+ works as well. It catches up on the days since its last run.
147
+
148
+ ### Next to garmin-intervals-bridge: the bridge says when there is something new
149
+
150
+ The bridge looks for new activities in Intervals every few minutes anyway, whatever their source. From
151
+ version 0.12.9 on it passes that on: set `BRIDGE_NOTIFY_URL` in the bridge's settings, and summits
152
+ syncs right after the bridge instead of looking every 15 minutes itself. On its own it then looks only
153
+ every 3 hours, in case the bridge stops. This needs `run --ui`, as in the Docker and Home Assistant
154
+ setups.
155
+
156
+ | The bridge runs … | `BRIDGE_NOTIFY_URL` |
157
+ |---|---|
158
+ | on the same computer | `http://127.0.0.1:8766/sync` |
159
+ | in the same `compose.yaml` | `http://summits:8766/sync` |
160
+ | as a Home Assistant app | `http://<hostname>:8766/sync`, the hostname is on the *Info* tab of the Intervals Summits app |
161
+
162
+ `POST /sync` only says "something is new": it takes no data, and summits keeps it to one sync a
163
+ minute. `intervals-summits status` shows the bridge's last note. On a Linux server with systemd,
164
+ [`deploy/intervals-summits-run.service`](deploy/intervals-summits-run.service) keeps it running
165
+ (instead of the timer in `deploy/`).
166
+
167
+ ### Commands
168
+
169
+ | Command | |
170
+ |---|---|
171
+ | `sync [--apply] [--force]` | The recent days; after a pause it reads back to the last run (at most 14 days) |
172
+ | `backfill --from DATE [--to DATE] [--apply]` | The past: tracks first, then OpenStreetMap for all of them at once, then matching and writing. Resumable. |
173
+ | `recompute --from DATE [--apply]` | Match again, e.g. after OpenStreetMap got better. Writes only differences. |
174
+ | `run [--apply] [--ui] [--host H] [--port 8766]` | Keeps running: a sync every 15 minutes (every 3 hours while the bridge reports new activities), optionally with the summit log page |
175
+ | `ui [--host H] [--port 8766]` | Only the summit log page |
176
+ | `page [--out FILE] [--language de]` | The summit log as one self-contained HTML file, to keep or share |
177
+ | `status [--check] [--fields]` | Counts and the last sync; `--check` for health checks (exit 1 when no sync came for long) |
178
+ | `fields [--apply]` | Create the activity fields now |
179
+
180
+ Without `--apply`, nothing is written to Intervals. The summit log page still fills.
181
+
182
+ `python examples/demo.py demo.html` builds a summit log with made-up data (the screenshot above).
183
+
184
+ ## Settings
185
+
186
+ | Variable | Default | |
187
+ |---|---|---|
188
+ | `INTERVALS_API_KEY` | – | Intervals → Settings → Developer Settings |
189
+ | `INTERVALS_ATHLETE_ID` | `0` | `0` = the key's owner |
190
+ | `SUMMITS_DATA_DIR` | `~/GarminBridge/summits` next to a bridge, else `~/IntervalsSummits` | database, tracks, `summits.html` |
191
+ | `SUMMITS_LANGUAGE` | `en` | `en` or `de`: names of new fields, the page, the description line |
192
+ | `SUMMITS_NAME_LANGUAGE` | – | e.g. `en`: OpenStreetMap's `name:en` where there is one, else the local name |
193
+ | `SUMMITS_FIELDS` | `Summits,SummitCount,HighestPoint` | add `HutsPasses` for huts and passes |
194
+ | `SUMMITS_DESCRIPTION` | `false` | `true`: the `⛰️ Summits:` line in the description (searchable) |
195
+ | `SUMMITS_TYPES` | every outdoor type | only these activity types, e.g. `Hike,BackcountrySki` |
196
+ | `SUMMITS_PEAK_RADIUS` | `100` | metres (10–200): how close the track must come to a summit |
197
+ | `SUMMITS_PEAK_BELOW` | `80` | metres (0–500): how far below the summit the track may be there |
198
+ | `SUMMITS_HUT_RADIUS` | `60` | metres (10–200) for huts |
199
+ | `SUMMITS_PASS_RADIUS` | `60` | metres (10–200) for passes |
200
+ | `SUMMITS_LOOKBACK_DAYS` | `3` | how far `sync` looks back |
201
+ | `SUMMITS_INTERVAL_MINUTES` | `15` | `run`: minutes between syncs |
202
+ | `SUMMITS_KEEP_TRACKS` | `true` | keep the tracks: `recompute` without downloading them again (~20 KB per activity) |
203
+ | `SUMMITS_OVERPASS_URL` | overpass-api.de | another Overpass server |
204
+ | `SUMMITS_PAGE_TITLE` | Summit log / Gipfelbuch | |
205
+
206
+ ## Fair to Intervals and OpenStreetMap
207
+
208
+ - **Intervals:** the API key is usually shared with the bridge. The tool paces itself (at most
209
+ about 1.5 requests a second) and waits whenever Intervals answers 429. A sync with nothing new is
210
+ one small request.
211
+ - **OpenStreetMap:** points come from the Overpass API and are cached in 0.2° cells for 180 days,
212
+ so your usual tours cost no query after the first time. A backfill asks for whole regions at
213
+ once (16 years and 1,800 tours, in the Alps and on a few trips abroad: 18 queries). One query at a time, with a pause between
214
+ queries, and patience on 429/5xx.
215
+
216
+ ## Privacy
217
+
218
+ Your data stays on your computer and in your Intervals account. Overpass learns which 0.2° cells
219
+ (about 22 × 15 km) you need, never a track or an activity. Details: [PRIVACY.md](PRIVACY.md).
220
+
221
+ ## Licence
222
+
223
+ MIT. Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, ODbL.
@@ -0,0 +1,204 @@
1
+ # intervals-summits
2
+
3
+ [![CI](https://github.com/futureweb/intervals-summits/actions/workflows/tests.yml/badge.svg)](https://github.com/futureweb/intervals-summits/actions/workflows/tests.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)
6
+ [![Garmin Intervals Bridge](https://img.shields.io/badge/companion-Garmin%20Intervals%20Bridge-6f42c1)](https://github.com/futureweb/garmin-intervals-bridge)
7
+ [![Futureweb Intervals MCP](https://img.shields.io/badge/companion-Futureweb%20Intervals%20MCP-0a7bbb)](https://github.com/futureweb/intervals-mcp-server)
8
+
9
+ **Summits, huts and passes on your [Intervals.icu](https://intervals.icu) activities**, found in your
10
+ own GPS tracks with OpenStreetMap, plus your personal **summit log**.
11
+
12
+ ![The summit log page (demo data)](docs/images/summit-log.png)
13
+
14
+ - **In Intervals:** every activity gets its summits in a field, e.g. `Maukspitze (2231 m), Ackerlspitze (2329 m)`.
15
+ Two more fields: the number of summits, which you can chart per week, month or year, and the
16
+ highest point of the activity.
17
+ - **The summit log page:**
18
+ - your summits, huts and passes over time;
19
+ - your top 10/20/50/100 (most visited or highest);
20
+ - a count per year;
21
+ - a searchable log, every entry linked to its activity in Intervals.
22
+ - **It works with every activity that has a GPS track**, whatever it came from: Garmin, Wahoo, Coros,
23
+ Suunto, Polar, a Strava import or an upload. Old ones too: one backfill matches your whole history.
24
+ - It needs nothing but your Intervals API key.
25
+
26
+ **Sister projects** (each works on its own, together they do more):
27
+
28
+ - [**Garmin Intervals Bridge**](https://github.com/futureweb/garmin-intervals-bridge) puts back what
29
+ Garmin strips from the FIT files it sends to Intervals.icu and syncs the wellness data the official
30
+ sync misses. intervals-summits runs next to it with the same settings, and the bridge tells it when
31
+ there is something new ([how](#next-to-garmin-intervals-bridge-the-bridge-says-when-there-is-something-new)).
32
+ - [**Futureweb Intervals MCP**](https://github.com/futureweb/intervals-mcp-server) lets ChatGPT, Claude
33
+ and any MCP client read and analyse your Intervals data. It reads the `Summits` fields like any other,
34
+ so you can ask your assistant which summits you climbed this year, or how often.
35
+
36
+ ## How a summit is found
37
+
38
+ - **Peaks** (`natural=peak`/`volcano`): the track comes within **100 m**, *and* the track's altitude
39
+ there is at most **80 m below** the summit. A path along the slope below a summit does not count;
40
+ a gondola ride to the top does.
41
+ - **Huts** (`tourism=alpine_hut`/`wilderness_hut`) and **passes** (`mountain_pass=yes`,
42
+ `natural=saddle`): within **60 m**.
43
+ - One entry per name and activity, in the order of the track.
44
+
45
+ These rules were calibrated against peakproject's summit lines on 25 activities: 38 of 40 summits
46
+ were found. A summit that OpenStreetMap does not have, or has under another name, cannot be found.
47
+ Improve the map, and `recompute` picks it up.
48
+
49
+ **Stricter or more generous:** the four numbers can be set (`SUMMITS_PEAK_RADIUS`,
50
+ `SUMMITS_PEAK_BELOW`, `SUMMITS_HUT_RADIUS`, `SUMMITS_PASS_RADIUS`, see [Settings](#settings)). For example
51
+ `SUMMITS_PEAK_RADIUS=50` and `SUMMITS_PEAK_BELOW=30` count only summits you were really on. Best set
52
+ them before the first backfill. Changed later, they apply to new activities at once, and
53
+ `recompute --from 2010-01-01` applies them to your past (from the stored tracks, without downloading them
54
+ again). An activity left with no summit at all keeps the values written before; clear those by hand.
55
+
56
+ ## What lands in Intervals
57
+
58
+ ![The fields on an activity page in Intervals](docs/images/intervals-activity.png)
59
+
60
+ | Field (code) | Type | Content |
61
+ |---|---|---|
62
+ | `Summits` | text | `Maukspitze (2231 m), Ackerlspitze (2329 m)`: the summits in the order you reached them |
63
+ | `SummitCount` | number, summed per day | how many summits; chart them per week, month or year |
64
+ | `HighestPoint` | number, max per day | **only where empty.** Intervals fills it from a FIT file itself; for GPX/TCX and old activities the tool takes the highest point of the track |
65
+ | `HutsPasses` | text | huts and passes. Off by default: `SUMMITS_FIELDS=…,HutsPasses` |
66
+ | description | one line | optional, `SUMMITS_DESCRIPTION=true`: `⛰️ Summits: Maukspitze (2231 m), …`, **so Intervals' search finds them** (see below) |
67
+
68
+ The tool creates missing fields itself.
69
+ - **Codes are the same for everyone**, so charts and scripts can be shared.
70
+ - **Names** are English, or German with `SUMMITS_LANGUAGE=de`. You may rename them in Intervals freely:
71
+ the tool goes by the code.
72
+
73
+ **Your edits stay.** A value you typed, or another tool wrote, is never overwritten (unless
74
+ `--force`). The tool only updates what it wrote itself. In the description it only ever touches its
75
+ own line: it reads the description again right before writing, adds the line at the end after a blank
76
+ line, and leaves every other character as it is. If the description already names all the summits
77
+ (peakproject's lines, for example), it adds nothing.
78
+
79
+ ### Seeing them in Intervals
80
+
81
+ - **On the activity:** the fields show above the charts. Which fields show is chosen per sport, and
82
+ they can go into the summary layout.
83
+ - **Filters:** in the activity list and on the totals page, filters can use custom fields, e.g. `SummitCount` at least 1 for all your summit days.
84
+ - **Fitness charts:** `SummitCount` (summed per day) and `HighestPoint` (maximum per day) can be
85
+ plotted; on a weekly or monthly scale you get summits per week or month.
86
+ - **Search:** Intervals' search looks at names and descriptions, not at custom fields. Turn on
87
+ `SUMMITS_DESCRIPTION=true`, and searching a summit finds every activity that reached it, also
88
+ where it is not in the name:
89
+
90
+ ![Intervals' search for "Schatterberg": tours that have it in no name](docs/images/intervals-search.png)
91
+
92
+ ## Installing and running
93
+
94
+ ### Docker (NAS, server, next to the bridge)
95
+
96
+ ```bash
97
+ curl -O https://raw.githubusercontent.com/futureweb/intervals-summits/main/compose.yaml
98
+ curl -o .env https://raw.githubusercontent.com/futureweb/intervals-summits/main/.env.example # put your key in
99
+ docker compose up -d # a dry run: the summit log fills, nothing is written
100
+ docker compose logs -f # what it would write
101
+ docker compose run --rm summits backfill --from 2015-01-01 --apply # your past
102
+ # then command: ["run", "--apply", "--ui", "--host", "0.0.0.0"] in compose.yaml and up -d again
103
+ ```
104
+
105
+ The summit log is at **http://localhost:8766/**. Next to the bridge, add the service to the bridge's
106
+ `compose.yaml` and use its `.env`: the same key works.
107
+
108
+ ### Home Assistant
109
+
110
+ Add `https://github.com/futureweb/intervals-summits` as an app repository (Settings → Apps → App store
111
+ → ⋮ → Repositories). Then install **Intervals Summits**, enter your Intervals API key, optionally
112
+ "Backfill from", and start it. The summit log appears in the sidebar. Switch on *Write to Intervals*
113
+ once the dry run looks right.
114
+
115
+ ### On your computer (Windows, Mac, Linux), Python 3.12+
116
+
117
+ ```bash
118
+ pip install intervals-summits # Windows: py -m pip install intervals-summits
119
+ intervals-summits backfill --from 2015-01-01 # dry run over your past
120
+ intervals-summits backfill --from 2015-01-01 --apply # … and write
121
+ intervals-summits run --apply --ui # keep running; the summit log on http://127.0.0.1:8766/
122
+ ```
123
+
124
+ Settings come from the environment, else from `./.env`, `~/IntervalsSummits/.env` or the bridge's
125
+ `~/GarminBridge/.env`. A bridge set up on the same computer is enough: nothing to configure twice.
126
+ Instead of keeping it running, `intervals-summits sync --apply` once a day (Task Scheduler, cron)
127
+ works as well. It catches up on the days since its last run.
128
+
129
+ ### Next to garmin-intervals-bridge: the bridge says when there is something new
130
+
131
+ The bridge looks for new activities in Intervals every few minutes anyway, whatever their source. From
132
+ version 0.12.9 on it passes that on: set `BRIDGE_NOTIFY_URL` in the bridge's settings, and summits
133
+ syncs right after the bridge instead of looking every 15 minutes itself. On its own it then looks only
134
+ every 3 hours, in case the bridge stops. This needs `run --ui`, as in the Docker and Home Assistant
135
+ setups.
136
+
137
+ | The bridge runs … | `BRIDGE_NOTIFY_URL` |
138
+ |---|---|
139
+ | on the same computer | `http://127.0.0.1:8766/sync` |
140
+ | in the same `compose.yaml` | `http://summits:8766/sync` |
141
+ | as a Home Assistant app | `http://<hostname>:8766/sync`, the hostname is on the *Info* tab of the Intervals Summits app |
142
+
143
+ `POST /sync` only says "something is new": it takes no data, and summits keeps it to one sync a
144
+ minute. `intervals-summits status` shows the bridge's last note. On a Linux server with systemd,
145
+ [`deploy/intervals-summits-run.service`](deploy/intervals-summits-run.service) keeps it running
146
+ (instead of the timer in `deploy/`).
147
+
148
+ ### Commands
149
+
150
+ | Command | |
151
+ |---|---|
152
+ | `sync [--apply] [--force]` | The recent days; after a pause it reads back to the last run (at most 14 days) |
153
+ | `backfill --from DATE [--to DATE] [--apply]` | The past: tracks first, then OpenStreetMap for all of them at once, then matching and writing. Resumable. |
154
+ | `recompute --from DATE [--apply]` | Match again, e.g. after OpenStreetMap got better. Writes only differences. |
155
+ | `run [--apply] [--ui] [--host H] [--port 8766]` | Keeps running: a sync every 15 minutes (every 3 hours while the bridge reports new activities), optionally with the summit log page |
156
+ | `ui [--host H] [--port 8766]` | Only the summit log page |
157
+ | `page [--out FILE] [--language de]` | The summit log as one self-contained HTML file, to keep or share |
158
+ | `status [--check] [--fields]` | Counts and the last sync; `--check` for health checks (exit 1 when no sync came for long) |
159
+ | `fields [--apply]` | Create the activity fields now |
160
+
161
+ Without `--apply`, nothing is written to Intervals. The summit log page still fills.
162
+
163
+ `python examples/demo.py demo.html` builds a summit log with made-up data (the screenshot above).
164
+
165
+ ## Settings
166
+
167
+ | Variable | Default | |
168
+ |---|---|---|
169
+ | `INTERVALS_API_KEY` | – | Intervals → Settings → Developer Settings |
170
+ | `INTERVALS_ATHLETE_ID` | `0` | `0` = the key's owner |
171
+ | `SUMMITS_DATA_DIR` | `~/GarminBridge/summits` next to a bridge, else `~/IntervalsSummits` | database, tracks, `summits.html` |
172
+ | `SUMMITS_LANGUAGE` | `en` | `en` or `de`: names of new fields, the page, the description line |
173
+ | `SUMMITS_NAME_LANGUAGE` | – | e.g. `en`: OpenStreetMap's `name:en` where there is one, else the local name |
174
+ | `SUMMITS_FIELDS` | `Summits,SummitCount,HighestPoint` | add `HutsPasses` for huts and passes |
175
+ | `SUMMITS_DESCRIPTION` | `false` | `true`: the `⛰️ Summits:` line in the description (searchable) |
176
+ | `SUMMITS_TYPES` | every outdoor type | only these activity types, e.g. `Hike,BackcountrySki` |
177
+ | `SUMMITS_PEAK_RADIUS` | `100` | metres (10–200): how close the track must come to a summit |
178
+ | `SUMMITS_PEAK_BELOW` | `80` | metres (0–500): how far below the summit the track may be there |
179
+ | `SUMMITS_HUT_RADIUS` | `60` | metres (10–200) for huts |
180
+ | `SUMMITS_PASS_RADIUS` | `60` | metres (10–200) for passes |
181
+ | `SUMMITS_LOOKBACK_DAYS` | `3` | how far `sync` looks back |
182
+ | `SUMMITS_INTERVAL_MINUTES` | `15` | `run`: minutes between syncs |
183
+ | `SUMMITS_KEEP_TRACKS` | `true` | keep the tracks: `recompute` without downloading them again (~20 KB per activity) |
184
+ | `SUMMITS_OVERPASS_URL` | overpass-api.de | another Overpass server |
185
+ | `SUMMITS_PAGE_TITLE` | Summit log / Gipfelbuch | |
186
+
187
+ ## Fair to Intervals and OpenStreetMap
188
+
189
+ - **Intervals:** the API key is usually shared with the bridge. The tool paces itself (at most
190
+ about 1.5 requests a second) and waits whenever Intervals answers 429. A sync with nothing new is
191
+ one small request.
192
+ - **OpenStreetMap:** points come from the Overpass API and are cached in 0.2° cells for 180 days,
193
+ so your usual tours cost no query after the first time. A backfill asks for whole regions at
194
+ once (16 years and 1,800 tours, in the Alps and on a few trips abroad: 18 queries). One query at a time, with a pause between
195
+ queries, and patience on 429/5xx.
196
+
197
+ ## Privacy
198
+
199
+ Your data stays on your computer and in your Intervals account. Overpass learns which 0.2° cells
200
+ (about 22 × 15 km) you need, never a track or an activity. Details: [PRIVACY.md](PRIVACY.md).
201
+
202
+ ## Licence
203
+
204
+ MIT. Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, ODbL.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "intervals-summits"
7
+ version = "0.1.0"
8
+ description = "Summits, huts and passes on your Intervals.icu activities, from your GPS tracks and OpenStreetMap – with a summit log page"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Andreas Schnederle-Wagner" }]
12
+ requires-python = ">=3.12"
13
+ dependencies = ["requests>=2.31"]
14
+ keywords = ["intervals.icu", "summits", "peaks", "openstreetmap", "hiking", "ski touring"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: GIS",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/futureweb/intervals-summits"
23
+
24
+ [project.scripts]
25
+ intervals-summits = "intervals_summits.cli:main"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8"]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Summits, huts and passes on Intervals.icu activities, from the GPS track and OpenStreetMap."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())