marfoerst/commuter
GitHub: marfoerst/commuter
一个自托管的通勤时间优化工具,通过定时采样 Google Routes API 生成热力图,帮助用户找到每周最佳出发时段并提供实时 REST API 供外部集成。
Stars: 0 | Forks: 0
# 通勤优化器
自托管通勤优化器。会在您的每周通勤时间窗口内对 Google Routes API 进行采样,将结果存储在 SQLite 中,并以热力图的形式显示最佳出发时间。它提供了一个小型 REST API,以便您喜欢的任何工具——仪表板、通知系统、聊天助手——都能消费这些数据。
## 功能特性
- FastAPI 后端 + 无需构建的 HTML/JS UI
- 两条命名路线——`morning`(家 → 公司)和 `evening`(公司 → 家),每条路线都有自己的采样窗口。Evening(傍晚)路线的地址会自动从 Morning(早晨)路线中反转。
- **严格的到达截止时间**(可选,按方向设置)。设置后,系统会推荐**仍能准时到达的最迟安全出发时间**,并显示按出发时间排名的前 3 个备选方案条。
- Google Routes API v2(`computeRoutes`,支持交通感知),用于获取实时行驶时间
- APScheduler 每天在可配置的时间(默认为 04:00)运行一次完整的每周重新计算
- 实时仪表板端点:每次请求都会并行发起两次 Routes API 调用——“立即出发”和建议时段——因此每次页面查看时数据都是最新的
- 在单个绑定挂载卷上持久化 SQLite 数据
- 单个 Docker 容器;可在任何运行 Docker 的地方运行(已在 Synology Container Manager 和 Docker Desktop 上测试)
## 项目布局
```
commuter/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example
└── app/
├── main.py # FastAPI app + lifespan
├── config.py # env config
├── api/routes.py # REST endpoints
├── services/
│ ├── google_routes.py # Routes API client
│ └── sampling.py # parallel sampler
├── scheduler/jobs.py # APScheduler daily job
├── db/ # SQLite schema + DAOs
└── static/ # index.html, styles.css, app.js
```
## 快速开始
1. 获取已启用 **Routes API** 的 Google Cloud API 密钥(Cloud Console → APIs & Services → Credentials)。将密钥限制为仅限 Routes API 和您服务器的 IP。
2. 将 `.env.example` 复制到 `.env` 并填入 `GOOGLE_API_KEY`。可以选择设置 `DEFAULT_ORIGIN` / `DEFAULT_DESTINATION`,以便在首次启动时自动填充这两条路线。
3. 构建并运行:
docker compose up -d --build
4. 打开 。在 **Setup**(设置)选项卡上,输入起点和终点,保存,然后点击 **Save & Recompute now**(保存并立即重新计算)以填充热力图。每日自动重新计算将随后使它们保持最新状态。
## 快速开始 (本地, 无 Docker)
```
python -m venv .venv
.venv\Scripts\activate # or: source .venv/bin/activate
pip install -r requirements.txt
# PowerShell:
$env:GOOGLE_API_KEY = "your_key"
$env:DATA_DIR = ".\data"
# bash:
# export GOOGLE_API_KEY=your_key
# export DATA_DIR=./data
uvicorn app.main:app --host 0.0.0.0 --port 8080
```
## 配置(环境变量)
| 变量 | 默认值 | 用途 |
| --------------------------- | ---------------------- | --------------------------------------------- |
| `GOOGLE_API_KEY` | _(必填)_ | 启用了 Routes API 的 Google Cloud 密钥 |
| `DEFAULT_ORIGIN` | _(空)_ | 首次启动时自动填充家庭住址 |
| `DEFAULT_DESTINATION` | _(空)_ | 首次启动时自动填充公司地址 |
| `TIME_WINDOW_START` | `07:00` | 早晨采样窗口开始时间 (HH:MM) |
| `TIME_WINDOW_END` | `09:00` | 早晨采样窗口结束时间 |
| `EVENING_TIME_WINDOW_START` | `16:00` | 傍晚采样窗口开始时间 |
| `EVENING_TIME_WINDOW_END` | `18:30` | 傍晚采样窗口结束时间 |
| `INTERVAL_MINUTES` | `10` | 采样间隔步长 |
| `DEFAULT_WEEKDAYS` | `Mon,Tue,Wed,Thu,Fri` | 需要采样的天数 |
| `SCHEDULER_HOUR` | `4` | 每日重新计算的小时 (使用 `TZ` 时区) |
| `SCHEDULER_MINUTE` | `0` | 每日重新计算的分钟 |
| `CONCURRENT_REQUESTS` | `10` | 每次重新计算时最大并行 Routes API 调用数 |
| `API_KEY` | _(空)_ | 如果设置,所有 `/api/*` 调用都需要 `X-API-Key` |
| `TZ` | `UTC` | 容器时区 |
| `DATA_DIR` | `/app/data` | SQLite + 持久化路径 |
## REST API
所有端点均返回 JSON。如果设置了 `API_KEY`,请在每个请求中发送 `X-API-Key: `。
### `GET /api/health`
```
{ "status": "ok" }
```
### `GET /api/config`
返回 `{ "morning": route|null, "evening": route|null }`。
### `POST /api/config`
一个家/公司地址对以及每个方向的采样窗口。Evening 路线将自动反转地址后进行存储。
```
{
"origin": "Berliner Str. 1, 10115 Berlin",
"destination": "Alexanderplatz, Berlin",
"morning": {
"time_window_start": "07:00",
"time_window_end": "09:00",
"interval_minutes": 10,
"weekdays": "Mon,Tue,Wed,Thu,Fri",
"arrival_deadline": "09:00"
},
"evening": {
"time_window_start": "16:00",
"time_window_end": "18:30",
"interval_minutes": 10,
"weekdays": "Mon,Tue,Wed,Thu,Fri",
"arrival_deadline": null
}
}
```
`arrival_deadline` 是可选的。当在某个方向上设置后,该方向的推荐将变为“在此时间之前到达的最晚出发时间”。
### `POST /api/recompute`
重新对两个方向进行采样。返回
`{ "status": "ok", "samples": {"morning": n, "evening": m} }`。
### `GET /api/commute/today`
两个方向的实时负载(在请求时查询 Routes API):
```
{
"morning": {
"name": "morning",
"day_of_week": "Thu",
"origin": "…",
"destination": "…",
"arrival_deadline": "09:00",
"best_departure_time": "08:00",
"optimal_duration": 59,
"arrival_time": "08:59",
"buffer_minutes": 1,
"current_duration": 42,
"time_savings": 14,
"live": true,
"alternatives": [
{ "departure_time": "08:00", "duration_minutes": 59, "arrival_time": "08:59", "buffer_minutes": 1 },
{ "departure_time": "07:50", "duration_minutes": 62, "arrival_time": "08:52", "buffer_minutes": 8 },
{ "departure_time": "07:40", "duration_minutes": 64, "arrival_time": "08:44", "buffer_minutes": 16 }
]
},
"evening": { "…": "same shape (no deadline → alternatives sorted by shortest drive)" }
}
```
`alternatives` 是来自今天快照的前 3 个候选时段,如果设置了截止时间则根据截止时间进行过滤;当应用了截止时间时,按最晚出发时间优先排序,否则按最短行驶时间优先排序。`best_departure_time` 即为 `alternatives[0]`。
### `GET /api/commute/today/{direction}`
形状相同但是扁平化的。`direction` 可以是 `morning` 或 `evening`。当上游消费者希望每次轮询只获取一个方向时(例如,它以自己的节奏轮询每个端点),这非常方便。
### `GET /api/commute/heatmap`
`{ "morning": [...], "evening": [...] }`,其中每个列表元素是
`{ "day": "Mon", "time": "07:00", "duration": 35.0 }`。
### `GET /api/commute/heatmap/{direction}`
单个方向的扁平化列表。
## 成本考量
每日重新计算会在每个方向上进行 `len(weekdays) × slots` 次 Routes API 调用,且 `routingPreference: TRAFFIC_AWARE_OPTIMAL`。在默认设置下(5 天 × 约 13 个时段 × 2 个方向 ≈ 130 次调用/天),您完全在 Google 针对个人用户的免费额度内,但请根据当前的 Routes API 定价进行核实。
每次加载仪表板也会执行 2 次实时 Routes API 调用(一次用于“立即出发”,一次用于“今日最佳剩余时段”)。为了降低成本,消费者应保持 `scan_interval >= 300` 秒的轮询间隔。
## 架构说明
- **采样逻辑** 会选择每个工作日/时间段的下一次未来出现时间,以便 Google Routes API 始终获得有效的未来 `departureTime`。
- **双层新鲜度**:每日 06:00 的批处理填充热力图(显示每周模式);实时仪表板端点在请求时再次查询 Google 以获取“立即出发”和今日最佳时段,因此每次查看的数字都能反映当前交通状况。
- **Schema 迁移** 是幂等的:现有的单路线部署将在下次启动时自动迁移到命名路线模型。
## 许可证
MIT — 请参阅 [LICENSE](LICENSE)。
标签:API采样, APScheduler, AV绕过, Docker, FastAPI, Google Routes API, NAS工具, Python, REST API, SQLite, Web界面, 交通路况, 仪表盘, 出行建议, 出行规划, 单容器, 多模态安全, 安全防御评估, 数据可视化, 数据持久化, 无后门, 热力图, 生活效率工具, 群晖, 自托管, 自驾工具, 请求拦截, 路线优化, 逆向工具, 通勤优化, 通勤助手