commute-optimizer 1.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.
- commute_optimizer-1.1.0/LICENSE +21 -0
- commute_optimizer-1.1.0/PKG-INFO +246 -0
- commute_optimizer-1.1.0/README.md +227 -0
- commute_optimizer-1.1.0/commute/__init__.py +0 -0
- commute_optimizer-1.1.0/commute/__main__.py +76 -0
- commute_optimizer-1.1.0/commute/check.py +84 -0
- commute_optimizer-1.1.0/commute/config.example.json +12 -0
- commute_optimizer-1.1.0/commute/config.py +76 -0
- commute_optimizer-1.1.0/commute/db.py +65 -0
- commute_optimizer-1.1.0/commute/demo.py +79 -0
- commute_optimizer-1.1.0/commute/poller.py +199 -0
- commute_optimizer-1.1.0/commute/report.py +302 -0
- commute_optimizer-1.1.0/commute/schedule.py +302 -0
- commute_optimizer-1.1.0/commute/watch.py +111 -0
- commute_optimizer-1.1.0/commute_optimizer.egg-info/PKG-INFO +246 -0
- commute_optimizer-1.1.0/commute_optimizer.egg-info/SOURCES.txt +19 -0
- commute_optimizer-1.1.0/commute_optimizer.egg-info/dependency_links.txt +1 -0
- commute_optimizer-1.1.0/commute_optimizer.egg-info/entry_points.txt +2 -0
- commute_optimizer-1.1.0/commute_optimizer.egg-info/top_level.txt +1 -0
- commute_optimizer-1.1.0/pyproject.toml +34 -0
- commute_optimizer-1.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Kahan
|
|
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,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commute-optimizer
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Find the best time to leave: track real drive times via the Google Routes API and report the optimal departure window
|
|
5
|
+
Author: Ben Kahan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bkahan/commute-optimizer
|
|
8
|
+
Project-URL: Issues, https://github.com/bkahan/commute-optimizer/issues
|
|
9
|
+
Keywords: commute,traffic,google-maps,routes-api,departure-time
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Commute Optimizer
|
|
21
|
+
|
|
22
|
+
Tracks the real drive time between two places by polling the Google Routes API
|
|
23
|
+
over a week, then tells you the best time to leave — with far tighter resolution
|
|
24
|
+
than the wide ranges Google Maps shows.
|
|
25
|
+
|
|
26
|
+
No dependencies beyond Python 3.9+.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install commute-optimizer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That gives you the `commute` command used below. Running from a source
|
|
33
|
+
checkout works too — substitute `python -m commute` for `commute`.
|
|
34
|
+
|
|
35
|
+
All data files (`config.json`, `.env`, `commute.db`, the reports, logs) live
|
|
36
|
+
in the directory you run `commute` from, so each tracking project can have its
|
|
37
|
+
own folder.
|
|
38
|
+
|
|
39
|
+
## Example output
|
|
40
|
+
|
|
41
|
+
### The report
|
|
42
|
+
|
|
43
|
+
`commute report` writes a self-contained `report.html`: one section
|
|
44
|
+
per direction, a chart of median commute vs. departure time (one line per
|
|
45
|
+
weekday), and tables ranking the best departure slot inside each rush window —
|
|
46
|
+
including how many minutes it saves over the worst slot.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
*(Generated from synthetic demo data — regenerate with
|
|
51
|
+
`python docs/_gen_example.py`.)*
|
|
52
|
+
|
|
53
|
+
### The terminal
|
|
54
|
+
|
|
55
|
+
Preflight check before a long run:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
> commute check
|
|
59
|
+
[ok] config: '1600 Amphitheatre Parkway, Mountain View, CA' -> '1 Market St, San Francisco, CA'
|
|
60
|
+
[ok] API key found (AIzaSy...XXXX)
|
|
61
|
+
[ok] database writable at C:\...\commute-optimizer\commute.db (0 existing samples)
|
|
62
|
+
[ok] live route -> 43.6 min now (free-flow 46.3 min, 58.5 km)
|
|
63
|
+
[ok] live route <- 43.1 min now (free-flow 46.5 min, 58.9 km)
|
|
64
|
+
[ok] estimated API usage: ~938/day, ~6566/week (free tier ~10,000/month)
|
|
65
|
+
|
|
66
|
+
All checks passed - ready for a long run.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Polling with the auto-stop timer:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
> commute poll -d 7d
|
|
73
|
+
Polling '1600 Amphitheatre Parkway, Mountain View, CA' <-> '1 Market St, San Francisco, CA'
|
|
74
|
+
Rush interval 1 min, off-peak 10 min. Ctrl+C to stop.
|
|
75
|
+
Will stop automatically at 2026-07-12 22:59 and generate the report.
|
|
76
|
+
[Sun 22:59:28] -> 43.6 min (free-flow 46.3 min)
|
|
77
|
+
[Sun 22:59:28] <- 43.1 min (free-flow 46.5 min)
|
|
78
|
+
...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
And when the report runs, it also prints the summary:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
1600 Amphitheatre Parkway, Mountain View, CA -> 1 Market St, San Francisco, CA
|
|
85
|
+
window 07:00-10:00:
|
|
86
|
+
Monday leave 09:45-10:00 (~28.7 min; worst 51.3 min, saves 22.6)
|
|
87
|
+
Tuesday leave 09:45-10:00 (~29.4 min; worst 51.5 min, saves 22.1)
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Setup
|
|
92
|
+
|
|
93
|
+
1. Get a Google Maps Platform API key with the **Routes API** enabled
|
|
94
|
+
(Google Cloud console → APIs & Services → enable "Routes API" → create an
|
|
95
|
+
API key).
|
|
96
|
+
|
|
97
|
+
2. In the folder where you want the data to live, write the example config
|
|
98
|
+
and edit your addresses:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
commute init
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. Put your API key in a `.env` file in the same folder:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
GOOGLE_MAPS_API_KEY=your-key-here
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
Before a long run, verify everything works (config, API key, database, one
|
|
113
|
+
live API call per direction, and a quota estimate for your poll schedule):
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
commute check
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Start polling. With `-d` it stops automatically after the given duration and
|
|
120
|
+
generates the report; without it, it runs until Ctrl+C:
|
|
121
|
+
|
|
122
|
+
```powershell
|
|
123
|
+
commute poll -d 7d # a full week, then auto-report
|
|
124
|
+
commute poll # open-ended
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Durations accept `d`/`h`/`m` and combinations: `7d`, `24h`, `90m`, `1d12h`.
|
|
128
|
+
Add `--no-report` to skip the automatic report at the end.
|
|
129
|
+
|
|
130
|
+
Generate the report any time mid-run (opens `report.html` in your browser;
|
|
131
|
+
`--no-open` to skip that):
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
commute report
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or watch the run continuously: this regenerates `report.html` *and* a
|
|
138
|
+
day-by-day report (`report-daily.html` — one curve per calendar date plus a
|
|
139
|
+
coverage table, so you can see the data filling in before the week is done).
|
|
140
|
+
Run it in a second terminal alongside `poll`; refresh the browser to see
|
|
141
|
+
updates:
|
|
142
|
+
|
|
143
|
+
```powershell
|
|
144
|
+
commute watch # updates every 15 minutes
|
|
145
|
+
commute watch -i 5m # or your own interval
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
To preview the report without waiting a week, seed a synthetic week of
|
|
149
|
+
two-direction data (refuses to run if the DB already has samples). In an
|
|
150
|
+
empty folder:
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
commute init
|
|
154
|
+
commute demo
|
|
155
|
+
commute report
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Surviving sleep and reboots
|
|
159
|
+
|
|
160
|
+
Instead of running `poll` in a terminal, register it with the OS so it stays
|
|
161
|
+
alive for the whole run (run this from your data folder — that's where the
|
|
162
|
+
scheduled poller will work):
|
|
163
|
+
|
|
164
|
+
```powershell
|
|
165
|
+
commute schedule install -d 7d # poll for a week, survive anything
|
|
166
|
+
commute schedule status # is the task/poller alive?
|
|
167
|
+
commute schedule remove # stop and unregister
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This relaunches the poller every 5 minutes (a lock file makes that a no-op
|
|
171
|
+
while one is already running) and resumes after a reboot once you log in. The
|
|
172
|
+
stop time is stored as an absolute timestamp, so it survives reboots too;
|
|
173
|
+
when it's reached, the report is generated as usual.
|
|
174
|
+
|
|
175
|
+
Per platform:
|
|
176
|
+
|
|
177
|
+
- **Windows** — a Task Scheduler task; it can also *wake the machine from
|
|
178
|
+
sleep* to poll. Output goes to `poll.log` (no console window).
|
|
179
|
+
- **Linux** — a systemd user service + timer (`commute-poll`). Logs go to
|
|
180
|
+
`journalctl --user -u commute-poll`. Runs from login onward; for
|
|
181
|
+
before-login polling run `loginctl enable-linger $USER`. Linux can't wake
|
|
182
|
+
itself from sleep for this; a missed tick fires right after wake.
|
|
183
|
+
- **macOS** — a launchd LaunchAgent (`com.commute-optimizer.poll`) logging to
|
|
184
|
+
`poll.log`. macOS won't wake for it either — to poll with the lid closed,
|
|
185
|
+
plug in and run `caffeinate -s`.
|
|
186
|
+
|
|
187
|
+
Don't run `schedule install` while a manually-started `poll` from an older
|
|
188
|
+
version is still running — stop that one first, or you'll get duplicate
|
|
189
|
+
samples.
|
|
190
|
+
|
|
191
|
+
If you skip all this and the machine sleeps mid-run, collected data is still
|
|
192
|
+
safe in `commute.db` — the report's Coverage section will show exactly which
|
|
193
|
+
stretches were missed.
|
|
194
|
+
|
|
195
|
+
## Config
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"origin": "home address",
|
|
200
|
+
"destination": "work address",
|
|
201
|
+
"travel_mode": "DRIVE",
|
|
202
|
+
"track_both_directions": true,
|
|
203
|
+
"poll": {
|
|
204
|
+
"active_window": ["00:00", "23:59"],
|
|
205
|
+
"rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
|
|
206
|
+
"rush_interval_min": 1,
|
|
207
|
+
"offpeak_interval_min": 10
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `track_both_directions` — when true (the default), every tick samples both
|
|
213
|
+
origin→destination and the reverse, so one run covers your morning and
|
|
214
|
+
evening commutes.
|
|
215
|
+
- `active_window` — only poll inside this local-time range (set e.g.
|
|
216
|
+
`["06:00", "20:00"]` to skip the night entirely).
|
|
217
|
+
- `rush_windows` — during these windows the poller samples every
|
|
218
|
+
`rush_interval_min` minutes; otherwise every `offpeak_interval_min` minutes.
|
|
219
|
+
The report ranks departure times *within* these windows (otherwise 3 a.m.
|
|
220
|
+
always wins).
|
|
221
|
+
|
|
222
|
+
## Cost
|
|
223
|
+
|
|
224
|
+
The Routes API (traffic-aware) has ~10,000 free calls/month. The default
|
|
225
|
+
adaptive schedule makes roughly 470 calls/day per direction — ~6,600/week with
|
|
226
|
+
both directions on, still inside the free tier but without room for a second
|
|
227
|
+
full week in the same month. Set `track_both_directions` to false or trim
|
|
228
|
+
`active_window` to halve it. Run `commute check` to see the estimate
|
|
229
|
+
for your exact schedule.
|
|
230
|
+
|
|
231
|
+
Samples land in `commute.db` (SQLite). Transient API failures (network blips,
|
|
232
|
+
rate limits, 5xx) are retried with backoff (5s/15s/45s) before being recorded
|
|
233
|
+
as an error row; permanent failures like a bad key fail fast without retries.
|
|
234
|
+
The report ends each direction with a **Coverage** section flagging any
|
|
235
|
+
stretch where sampling stopped (sleep, reboot, repeated errors) despite the
|
|
236
|
+
schedule expecting data — so you know which medians to trust.
|
|
237
|
+
|
|
238
|
+
## Known limitations (v1)
|
|
239
|
+
|
|
240
|
+
- **Multi-week data blends together.** The report groups by weekday, so two
|
|
241
|
+
Mondays from different weeks merge into one median curve. Intended use is a
|
|
242
|
+
single week per `commute.db`; delete or rename the DB between runs.
|
|
243
|
+
- **Wake-from-sleep is Windows-only.** On Linux/macOS the scheduled poller
|
|
244
|
+
resumes right after wake but can't wake the machine itself.
|
|
245
|
+
- **No automated tests.** The pieces are small and hand-verified, but there's
|
|
246
|
+
no test suite yet.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Commute Optimizer
|
|
2
|
+
|
|
3
|
+
Tracks the real drive time between two places by polling the Google Routes API
|
|
4
|
+
over a week, then tells you the best time to leave — with far tighter resolution
|
|
5
|
+
than the wide ranges Google Maps shows.
|
|
6
|
+
|
|
7
|
+
No dependencies beyond Python 3.9+.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install commute-optimizer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That gives you the `commute` command used below. Running from a source
|
|
14
|
+
checkout works too — substitute `python -m commute` for `commute`.
|
|
15
|
+
|
|
16
|
+
All data files (`config.json`, `.env`, `commute.db`, the reports, logs) live
|
|
17
|
+
in the directory you run `commute` from, so each tracking project can have its
|
|
18
|
+
own folder.
|
|
19
|
+
|
|
20
|
+
## Example output
|
|
21
|
+
|
|
22
|
+
### The report
|
|
23
|
+
|
|
24
|
+
`commute report` writes a self-contained `report.html`: one section
|
|
25
|
+
per direction, a chart of median commute vs. departure time (one line per
|
|
26
|
+
weekday), and tables ranking the best departure slot inside each rush window —
|
|
27
|
+
including how many minutes it saves over the worst slot.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
*(Generated from synthetic demo data — regenerate with
|
|
32
|
+
`python docs/_gen_example.py`.)*
|
|
33
|
+
|
|
34
|
+
### The terminal
|
|
35
|
+
|
|
36
|
+
Preflight check before a long run:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
> commute check
|
|
40
|
+
[ok] config: '1600 Amphitheatre Parkway, Mountain View, CA' -> '1 Market St, San Francisco, CA'
|
|
41
|
+
[ok] API key found (AIzaSy...XXXX)
|
|
42
|
+
[ok] database writable at C:\...\commute-optimizer\commute.db (0 existing samples)
|
|
43
|
+
[ok] live route -> 43.6 min now (free-flow 46.3 min, 58.5 km)
|
|
44
|
+
[ok] live route <- 43.1 min now (free-flow 46.5 min, 58.9 km)
|
|
45
|
+
[ok] estimated API usage: ~938/day, ~6566/week (free tier ~10,000/month)
|
|
46
|
+
|
|
47
|
+
All checks passed - ready for a long run.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Polling with the auto-stop timer:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
> commute poll -d 7d
|
|
54
|
+
Polling '1600 Amphitheatre Parkway, Mountain View, CA' <-> '1 Market St, San Francisco, CA'
|
|
55
|
+
Rush interval 1 min, off-peak 10 min. Ctrl+C to stop.
|
|
56
|
+
Will stop automatically at 2026-07-12 22:59 and generate the report.
|
|
57
|
+
[Sun 22:59:28] -> 43.6 min (free-flow 46.3 min)
|
|
58
|
+
[Sun 22:59:28] <- 43.1 min (free-flow 46.5 min)
|
|
59
|
+
...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
And when the report runs, it also prints the summary:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
1600 Amphitheatre Parkway, Mountain View, CA -> 1 Market St, San Francisco, CA
|
|
66
|
+
window 07:00-10:00:
|
|
67
|
+
Monday leave 09:45-10:00 (~28.7 min; worst 51.3 min, saves 22.6)
|
|
68
|
+
Tuesday leave 09:45-10:00 (~29.4 min; worst 51.5 min, saves 22.1)
|
|
69
|
+
...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Setup
|
|
73
|
+
|
|
74
|
+
1. Get a Google Maps Platform API key with the **Routes API** enabled
|
|
75
|
+
(Google Cloud console → APIs & Services → enable "Routes API" → create an
|
|
76
|
+
API key).
|
|
77
|
+
|
|
78
|
+
2. In the folder where you want the data to live, write the example config
|
|
79
|
+
and edit your addresses:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
commute init
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
3. Put your API key in a `.env` file in the same folder:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
GOOGLE_MAPS_API_KEY=your-key-here
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
Before a long run, verify everything works (config, API key, database, one
|
|
94
|
+
live API call per direction, and a quota estimate for your poll schedule):
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
commute check
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Start polling. With `-d` it stops automatically after the given duration and
|
|
101
|
+
generates the report; without it, it runs until Ctrl+C:
|
|
102
|
+
|
|
103
|
+
```powershell
|
|
104
|
+
commute poll -d 7d # a full week, then auto-report
|
|
105
|
+
commute poll # open-ended
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Durations accept `d`/`h`/`m` and combinations: `7d`, `24h`, `90m`, `1d12h`.
|
|
109
|
+
Add `--no-report` to skip the automatic report at the end.
|
|
110
|
+
|
|
111
|
+
Generate the report any time mid-run (opens `report.html` in your browser;
|
|
112
|
+
`--no-open` to skip that):
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
commute report
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or watch the run continuously: this regenerates `report.html` *and* a
|
|
119
|
+
day-by-day report (`report-daily.html` — one curve per calendar date plus a
|
|
120
|
+
coverage table, so you can see the data filling in before the week is done).
|
|
121
|
+
Run it in a second terminal alongside `poll`; refresh the browser to see
|
|
122
|
+
updates:
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
commute watch # updates every 15 minutes
|
|
126
|
+
commute watch -i 5m # or your own interval
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
To preview the report without waiting a week, seed a synthetic week of
|
|
130
|
+
two-direction data (refuses to run if the DB already has samples). In an
|
|
131
|
+
empty folder:
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
commute init
|
|
135
|
+
commute demo
|
|
136
|
+
commute report
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Surviving sleep and reboots
|
|
140
|
+
|
|
141
|
+
Instead of running `poll` in a terminal, register it with the OS so it stays
|
|
142
|
+
alive for the whole run (run this from your data folder — that's where the
|
|
143
|
+
scheduled poller will work):
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
commute schedule install -d 7d # poll for a week, survive anything
|
|
147
|
+
commute schedule status # is the task/poller alive?
|
|
148
|
+
commute schedule remove # stop and unregister
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This relaunches the poller every 5 minutes (a lock file makes that a no-op
|
|
152
|
+
while one is already running) and resumes after a reboot once you log in. The
|
|
153
|
+
stop time is stored as an absolute timestamp, so it survives reboots too;
|
|
154
|
+
when it's reached, the report is generated as usual.
|
|
155
|
+
|
|
156
|
+
Per platform:
|
|
157
|
+
|
|
158
|
+
- **Windows** — a Task Scheduler task; it can also *wake the machine from
|
|
159
|
+
sleep* to poll. Output goes to `poll.log` (no console window).
|
|
160
|
+
- **Linux** — a systemd user service + timer (`commute-poll`). Logs go to
|
|
161
|
+
`journalctl --user -u commute-poll`. Runs from login onward; for
|
|
162
|
+
before-login polling run `loginctl enable-linger $USER`. Linux can't wake
|
|
163
|
+
itself from sleep for this; a missed tick fires right after wake.
|
|
164
|
+
- **macOS** — a launchd LaunchAgent (`com.commute-optimizer.poll`) logging to
|
|
165
|
+
`poll.log`. macOS won't wake for it either — to poll with the lid closed,
|
|
166
|
+
plug in and run `caffeinate -s`.
|
|
167
|
+
|
|
168
|
+
Don't run `schedule install` while a manually-started `poll` from an older
|
|
169
|
+
version is still running — stop that one first, or you'll get duplicate
|
|
170
|
+
samples.
|
|
171
|
+
|
|
172
|
+
If you skip all this and the machine sleeps mid-run, collected data is still
|
|
173
|
+
safe in `commute.db` — the report's Coverage section will show exactly which
|
|
174
|
+
stretches were missed.
|
|
175
|
+
|
|
176
|
+
## Config
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"origin": "home address",
|
|
181
|
+
"destination": "work address",
|
|
182
|
+
"travel_mode": "DRIVE",
|
|
183
|
+
"track_both_directions": true,
|
|
184
|
+
"poll": {
|
|
185
|
+
"active_window": ["00:00", "23:59"],
|
|
186
|
+
"rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
|
|
187
|
+
"rush_interval_min": 1,
|
|
188
|
+
"offpeak_interval_min": 10
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `track_both_directions` — when true (the default), every tick samples both
|
|
194
|
+
origin→destination and the reverse, so one run covers your morning and
|
|
195
|
+
evening commutes.
|
|
196
|
+
- `active_window` — only poll inside this local-time range (set e.g.
|
|
197
|
+
`["06:00", "20:00"]` to skip the night entirely).
|
|
198
|
+
- `rush_windows` — during these windows the poller samples every
|
|
199
|
+
`rush_interval_min` minutes; otherwise every `offpeak_interval_min` minutes.
|
|
200
|
+
The report ranks departure times *within* these windows (otherwise 3 a.m.
|
|
201
|
+
always wins).
|
|
202
|
+
|
|
203
|
+
## Cost
|
|
204
|
+
|
|
205
|
+
The Routes API (traffic-aware) has ~10,000 free calls/month. The default
|
|
206
|
+
adaptive schedule makes roughly 470 calls/day per direction — ~6,600/week with
|
|
207
|
+
both directions on, still inside the free tier but without room for a second
|
|
208
|
+
full week in the same month. Set `track_both_directions` to false or trim
|
|
209
|
+
`active_window` to halve it. Run `commute check` to see the estimate
|
|
210
|
+
for your exact schedule.
|
|
211
|
+
|
|
212
|
+
Samples land in `commute.db` (SQLite). Transient API failures (network blips,
|
|
213
|
+
rate limits, 5xx) are retried with backoff (5s/15s/45s) before being recorded
|
|
214
|
+
as an error row; permanent failures like a bad key fail fast without retries.
|
|
215
|
+
The report ends each direction with a **Coverage** section flagging any
|
|
216
|
+
stretch where sampling stopped (sleep, reboot, repeated errors) despite the
|
|
217
|
+
schedule expecting data — so you know which medians to trust.
|
|
218
|
+
|
|
219
|
+
## Known limitations (v1)
|
|
220
|
+
|
|
221
|
+
- **Multi-week data blends together.** The report groups by weekday, so two
|
|
222
|
+
Mondays from different weeks merge into one median curve. Intended use is a
|
|
223
|
+
single week per `commute.db`; delete or rename the DB between runs.
|
|
224
|
+
- **Wake-from-sleep is Windows-only.** On Linux/macOS the scheduled poller
|
|
225
|
+
resumes right after wake but can't wake the machine itself.
|
|
226
|
+
- **No automated tests.** The pieces are small and hand-verified, but there's
|
|
227
|
+
no test suite yet.
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""CLI entry point: python -m commute {poll|report}"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _version():
|
|
7
|
+
try:
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
return version("commute-optimizer")
|
|
10
|
+
except Exception: # running from a source checkout without install
|
|
11
|
+
return "dev"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
p = argparse.ArgumentParser(prog="commute",
|
|
16
|
+
description="Track commute times and find the best departure time.")
|
|
17
|
+
p.add_argument("-c", "--config", help="Path to config.json (default: ./config.json)")
|
|
18
|
+
p.add_argument("--version", action="version", version=f"commute {_version()}")
|
|
19
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
20
|
+
sub.add_parser("init", help="Write an example config.json to the current directory")
|
|
21
|
+
pp = sub.add_parser("poll", help="Start the adaptive polling loop (Ctrl+C to stop)")
|
|
22
|
+
pp.add_argument("-d", "--duration",
|
|
23
|
+
help="Auto-stop after this long and generate the report "
|
|
24
|
+
"(e.g. 7d, 24h, 90m, 1d12h)")
|
|
25
|
+
pp.add_argument("--no-report", action="store_true",
|
|
26
|
+
help="Don't auto-generate the report when the duration ends")
|
|
27
|
+
pp.add_argument("--until",
|
|
28
|
+
help="Auto-stop at this ISO timestamp (what 'schedule' uses "
|
|
29
|
+
"so the deadline survives reboots)")
|
|
30
|
+
rp = sub.add_parser("report", help="Analyze samples and generate report.html")
|
|
31
|
+
rp.add_argument("--no-open", action="store_true", help="Don't open the report in a browser")
|
|
32
|
+
wp = sub.add_parser("watch",
|
|
33
|
+
help="Watch an in-progress run: regenerate the report and a "
|
|
34
|
+
"day-by-day report on an interval")
|
|
35
|
+
wp.add_argument("-i", "--interval", default="15m",
|
|
36
|
+
help="How often to regenerate (default 15m)")
|
|
37
|
+
wp.add_argument("--no-open", action="store_true",
|
|
38
|
+
help="Don't open report-daily.html in a browser")
|
|
39
|
+
sp = sub.add_parser("schedule",
|
|
40
|
+
help="Keep polling alive across sleep and reboots via a "
|
|
41
|
+
"scheduled task (Windows)")
|
|
42
|
+
sp.add_argument("action", choices=["install", "remove", "status"])
|
|
43
|
+
sp.add_argument("-d", "--duration",
|
|
44
|
+
help="With install: stop polling this long from now and "
|
|
45
|
+
"generate the report (e.g. 7d)")
|
|
46
|
+
sub.add_parser("check", help="Preflight: verify config, API key, DB, and live API access")
|
|
47
|
+
sub.add_parser("demo", help="Seed the DB with a synthetic week of two-direction data")
|
|
48
|
+
args = p.parse_args()
|
|
49
|
+
|
|
50
|
+
if args.cmd == "init":
|
|
51
|
+
from .config import init_config
|
|
52
|
+
init_config(args.config)
|
|
53
|
+
elif args.cmd == "poll":
|
|
54
|
+
from . import poller
|
|
55
|
+
poller.run(args.config, duration=args.duration,
|
|
56
|
+
report_on_end=not args.no_report, until=args.until)
|
|
57
|
+
elif args.cmd == "schedule":
|
|
58
|
+
from . import schedule
|
|
59
|
+
schedule.run(args.action, args.config, args.duration)
|
|
60
|
+
elif args.cmd == "watch":
|
|
61
|
+
from . import watch
|
|
62
|
+
watch.run(args.config, interval=args.interval,
|
|
63
|
+
open_browser=not args.no_open)
|
|
64
|
+
elif args.cmd == "check":
|
|
65
|
+
from . import check
|
|
66
|
+
raise SystemExit(check.run(args.config))
|
|
67
|
+
elif args.cmd == "demo":
|
|
68
|
+
from . import demo
|
|
69
|
+
demo.run(args.config)
|
|
70
|
+
else:
|
|
71
|
+
from . import report
|
|
72
|
+
report.run(args.config, open_browser=not args.no_open)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Preflight check: validate config, API key, database, and make one real
|
|
2
|
+
Routes API call per direction before committing to a week-long run."""
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from . import db
|
|
7
|
+
from .config import load_api_key, load_config
|
|
8
|
+
from .poller import current_interval, fetch_route
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _estimate_calls_per_day(cfg):
|
|
12
|
+
"""Walk a synthetic day in 1-minute steps and count when a poll would fire."""
|
|
13
|
+
calls = 0
|
|
14
|
+
t = datetime(2026, 1, 5, 0, 0) # any Monday; only time-of-day matters
|
|
15
|
+
next_poll = t
|
|
16
|
+
while t.date() == datetime(2026, 1, 5).date():
|
|
17
|
+
interval = current_interval(cfg, t)
|
|
18
|
+
if interval is not None and t >= next_poll:
|
|
19
|
+
calls += 1
|
|
20
|
+
next_poll = t + timedelta(seconds=interval)
|
|
21
|
+
t += timedelta(minutes=1)
|
|
22
|
+
return calls * (2 if cfg.get("track_both_directions", True) else 1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(config_path=None):
|
|
26
|
+
ok = True
|
|
27
|
+
|
|
28
|
+
# 1. config
|
|
29
|
+
try:
|
|
30
|
+
cfg = load_config(config_path)
|
|
31
|
+
print(f"[ok] config: {cfg['origin']!r} -> {cfg['destination']!r}")
|
|
32
|
+
except SystemExit as e:
|
|
33
|
+
print(f"[FAIL] config: {e}")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
# 2. API key present
|
|
37
|
+
try:
|
|
38
|
+
api_key = load_api_key()
|
|
39
|
+
print(f"[ok] API key found ({api_key[:6]}...{api_key[-4:]})")
|
|
40
|
+
except SystemExit as e:
|
|
41
|
+
print(f"[FAIL] API key: {e}")
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
# 3. database writable
|
|
45
|
+
try:
|
|
46
|
+
conn = db.connect()
|
|
47
|
+
conn.execute("SELECT 1")
|
|
48
|
+
n = db.count_samples(conn)
|
|
49
|
+
conn.close()
|
|
50
|
+
print(f"[ok] database writable at {db.DB_PATH} ({n} existing samples)")
|
|
51
|
+
if n:
|
|
52
|
+
print(" note: existing samples will mix into the report — "
|
|
53
|
+
"delete commute.db for a clean run.")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"[FAIL] database: {e!r}")
|
|
56
|
+
ok = False
|
|
57
|
+
|
|
58
|
+
# 4. live API call per direction
|
|
59
|
+
directions = [("ab", cfg["origin"], cfg["destination"])]
|
|
60
|
+
if cfg.get("track_both_directions", True):
|
|
61
|
+
directions.append(("ba", cfg["destination"], cfg["origin"]))
|
|
62
|
+
for name, origin, destination in directions:
|
|
63
|
+
arrow = "->" if name == "ab" else "<-"
|
|
64
|
+
try:
|
|
65
|
+
r = fetch_route(api_key, origin, destination,
|
|
66
|
+
cfg.get("travel_mode", "DRIVE"))
|
|
67
|
+
print(f"[ok] live route {arrow} {r['duration_s'] / 60:.1f} min now "
|
|
68
|
+
f"(free-flow {r['static_duration_s'] / 60:.1f} min, "
|
|
69
|
+
f"{r['distance_m'] / 1000:.1f} km)")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
msg = getattr(e, "read", None)
|
|
72
|
+
detail = msg().decode(errors="replace")[:300] if msg else repr(e)
|
|
73
|
+
print(f"[FAIL] live route {arrow}: {detail}")
|
|
74
|
+
ok = False
|
|
75
|
+
|
|
76
|
+
# 5. quota estimate
|
|
77
|
+
per_day = _estimate_calls_per_day(cfg)
|
|
78
|
+
per_week = per_day * 7
|
|
79
|
+
print(f"[{'ok' if per_week < 9500 else 'WARN'}] estimated API usage: "
|
|
80
|
+
f"~{per_day}/day, ~{per_week}/week (free tier ~10,000/month)")
|
|
81
|
+
|
|
82
|
+
print("\nAll checks passed — ready for a long run."
|
|
83
|
+
if ok else "\nFix the failures above before starting a long run.")
|
|
84
|
+
return 0 if ok else 1
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"origin": "1600 Amphitheatre Parkway, Mountain View, CA",
|
|
3
|
+
"destination": "1 Market St, San Francisco, CA",
|
|
4
|
+
"travel_mode": "DRIVE",
|
|
5
|
+
"track_both_directions": true,
|
|
6
|
+
"poll": {
|
|
7
|
+
"active_window": ["00:00", "23:59"],
|
|
8
|
+
"rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
|
|
9
|
+
"rush_interval_min": 1,
|
|
10
|
+
"offpeak_interval_min": 10
|
|
11
|
+
}
|
|
12
|
+
}
|