Improve first-run and channel setup for non-technical users: detect newer Gemini key formats, pin Node 22.22.3, add Control Panel Telegram approve flow, and keep channels/models when config is rewritten. Persist uclaw wizard state via uclaw-meta.json so restarts skip language/persona prompts. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
3.0 KiB
Markdown
83 lines
3.0 KiB
Markdown
---
|
|
name: sg-transport
|
|
description: "Singapore transport - bus arrival times, MRT service alerts, carpark availability, traffic. Uses LTA DataMall."
|
|
metadata: { "openclaw": { "emoji": "🚇" } }
|
|
---
|
|
|
|
# Singapore Transport
|
|
|
|
Live bus, MRT and traffic data from LTA DataMall.
|
|
|
|
## Before anything works: a key
|
|
|
|
Unlike the weather API, LTA needs one. It is free.
|
|
|
|
1. Register at https://datamall.lta.gov.sg/content/datamall/en/request-for-api.html
|
|
2. The AccountKey arrives by email
|
|
3. Store it as `LTA_ACCOUNT_KEY` — never paste it into a message or a filename
|
|
|
|
Every request sends it as the `AccountKey` header.
|
|
|
|
## What it does
|
|
|
|
- **Bus arrivals** — next three buses at a stop, with crowding
|
|
- **Train alerts** — live disruptions and advisories
|
|
- **Carpark availability** — free lots, useful before driving to a mall
|
|
- **Travel times** — expressway estimates
|
|
|
|
## How to run it
|
|
|
|
```bash
|
|
# Next buses at a stop. Bus stop codes are the 5 digits on the pole.
|
|
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
|
|
"https://datamall2.mytransport.sg/ltaodataservice/v3/BusArrival?BusStopCode=83139" \
|
|
| python3 -c "
|
|
import json,sys,datetime
|
|
d=json.load(sys.stdin)
|
|
now=datetime.datetime.now(datetime.timezone.utc)
|
|
for svc in d.get('Services',[]):
|
|
mins=[]
|
|
for k in ('NextBus','NextBus2','NextBus3'):
|
|
t=svc.get(k,{}).get('EstimatedArrival')
|
|
if not t: continue
|
|
eta=datetime.datetime.fromisoformat(t)
|
|
mins.append(max(0,round((eta-now).total_seconds()/60)))
|
|
load={'SEA':'seats','SDA':'standing','LSD':'packed'}.get(svc['NextBus'].get('Load'),'')
|
|
print(f\"Bus {svc['ServiceNo']:5} {', '.join(str(m)+' min' for m in mins) or 'no data':30} {load}\")
|
|
"
|
|
|
|
# Train disruptions right now
|
|
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
|
|
"https://datamall2.mytransport.sg/ltaodataservice/TrainServiceAlerts" \
|
|
| python3 -c "
|
|
import json,sys
|
|
d=json.load(sys.stdin)['value']
|
|
print('Normal service' if d['Status']==1 else 'DISRUPTION')
|
|
for m in d.get('Message',[]): print('-', m['Content'])
|
|
"
|
|
|
|
# Carpark availability, filtered by name
|
|
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
|
|
"https://datamall2.mytransport.sg/ltaodataservice/CarParkAvailabilityv2" \
|
|
| python3 -c "
|
|
import json,sys
|
|
want='VIVOCITY' # change this
|
|
for c in json.load(sys.stdin)['value']:
|
|
if want.lower() in c['Development'].lower():
|
|
print(f\"{c['Development']:40} {c['AvailableLots']:5} lots\")
|
|
"
|
|
```
|
|
|
|
## Working notes
|
|
|
|
- **Ask for the bus stop code** rather than guessing from a road name. It is
|
|
printed on the pole and on the app; guessing sends someone to the wrong stop.
|
|
- Arrival times come as absolute timestamps; convert to "in N minutes", which is
|
|
what the person actually wants to know.
|
|
- Crowding codes: `SEA` seats available · `SDA` standing only · `LSD` limited
|
|
standing. Translate them — the codes mean nothing to a rider.
|
|
- Responses cap at 500 records; the carpark and bus-stop lists are paged with
|
|
`?$skip=500`.
|
|
- If the key is missing or rejected, say which and point at the registration
|
|
page. Do not silently return nothing.
|