Skip to content

tennisscores: new app - live tennis scores (Bangle.js 2) - #4316

Merged
thyttan merged 2 commits into
espruino:masterfrom
bensynapse:add-tennisscores
Aug 16, 2026
Merged

tennisscores: new app - live tennis scores (Bangle.js 2)#4316
thyttan merged 2 commits into
espruino:masterfrom
bensynapse:add-tennisscores

Conversation

@bensynapse

Copy link
Copy Markdown
Contributor

Adds apps/tennisscoresTennis Scores for Bangle.js 2: live tennis scores on the watch.

  • When matches are live: one match per page — tournament, round, both players, games per set, current points, and a dot next to the serving player. Swipe up/down to page, tap to refresh.
  • When nothing is live: the next scheduled fixtures instead.
  • Optional auto-refresh while the app is open (off by default, 15 min default interval — ~96 requests for a full day of continuous refreshing, which fits the API free tier's 100 requests/day).
  • Data comes from the Live Tennis API over Bangle.http, so it needs an internet-enabled Gadgetbridge version (same requirement as owmweather) and a free API key entered via settings (text input library needed, again like owmweather).

Screenshots (taken in the headless emulator with a stubbed Bangle.http):

live match
fixtures

bin/sanitycheck.js passes (0 errors, 0 warnings) and eslint apps/tennisscores --max-warnings 0 is clean.

Disclosure: I maintain the Live Tennis API the app talks to.

🤖 Generated with Claude Code

Shows live matches (score, serving player) for a configurable tour,
and the next scheduled fixtures when nothing is live. Manual refresh
plus optional 15-minute auto refresh. Fetches over Bangle.http, so it
needs an internet-enabled Gadgetbridge, and a (free) Live Tennis API
key set in settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

I suggest adding an app loader interface for adding the api-key. Makes it easier to enter the key - the keyboard I use on the watch doesn't even have all necessary characters!

owmweather does it like this: https://github.com/espruino/BangleApps/blob/HEAD/apps/owmweather/interface.html

Here's a tutorial on app loader interfaces: https://www.espruino.com/Bangle.js+Storage

There's also this, that maybe I would not use for this case but anyway: https://www.espruino.com/Bangle.js+Custom

Suggested by @thyttan - the watch keyboard may not even have all the
characters an API key needs. interface.html (modeled on owmweather's)
loads tennisscores.json, lets you paste the key and pick a tour from
the browser, and writes it back. On-watch settings entry stays as a
fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bensynapse

Copy link
Copy Markdown
Contributor Author

Great point about the watch keyboard, thanks @thyttan — an API key on the on-watch keyboard is no fun (or, as you say, not even possible on some keyboards).

Added in 07b0872: interface.html modeled on owmweather's — it loads the current tennisscores.json, lets you paste the API key and pick the tour from the browser, and writes it back via the storage API. Registered with "interface" in metadata.json, and the README now points to it as the easiest way to enter the key. The on-watch settings entry stays as a fallback for anyone without the loader at hand.

@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Minor thing, the timeout error is a bit hard to read on the watch screen:

image

@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

I use gadgetbridge nightly with internet helper app and have this problem:

[image removed]

@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Logcat messages from Android Studio:

characteristic changed: 6e400003-b5a3-f393-e0a9-e50e24dcca9e - 0D0A7B2274223A2268747470222C2275726C223A2268747470733A2F2F6170692E6C69766574656E6E69736170692E636F6D2F6170692F7075626C69632F76312F6D6174636865733F7374617475733D6C697665266C696D69743D3130222C226964223A223537323030353637383839222C2268656164657273223A7B224175
RX: 
{"t":"http","url":"https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=10","id":"57200567889","headers":{"Au
UART RX LINE: 
characteristic changed: 6e400003-b5a3-f393-e0a9-e50e24dcca9e - 74686F72697A6174696F6E223A224265617265722074776A705F6535343432623333376161326539366266393939373836303661633037616635227D2C2274696D656F7574223A31353030307D0D0A
RX: thorization":"Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxx"},"timeout":15000}
UART RX LINE: {"t":"http","url":"https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=10","id":"57200567889","headers":{"Authorization":"Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"timeout":15000}
Forwarding GET request to https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=10 to internet helper app
GET https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=10 (allowInsecure=false, headers={Authorization=Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, User-Agent=Gadgetbridge/0.92.0-6c2947eca (6c2947eca)})
Request failed: Failed to connect to api.livetennisapi.com/0.0.0.0:443
UART TX: �GB({"t":"http","id":"57200567889"})
add: 23:03:16 Transaction with 1 actions for http
UART RX JSON parsed successfully
execute: 23:03:16 WriteAction 6e400002-b5a3-f393-e0a9-e50e24dcca9e - 104742287B2274223A2268747470222C226964223A223537323030353637383839227D290A
characteristic written: 6e400002-b5a3-f393-e0a9-e50e24dcca9e GATT_SUCCESS
characteristic changed: 6e400003-b5a3-f393-e0a9-e50e24dcca9e - 0D1B5B4A3E
RX: �[J>

@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

From terminal on my laptop the api returns alright:

curl -X GET "https://api.livetennisapi.com/api/public/v1/matches?status=live&limit=10" \
                           -H "Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
                           -H "User-Agent: Gadgetbridge/0.92.0-6c2947eca (6c2947eca)"

{"data":[{"event_status":null,"format":"BO3","id":175033,"indoor":false,"is_doubles":false,"is_qualifying":null,"players":{"p1":{"backhand":null,"birthday":"2003-09-08","country":"bul","data_completeness":{"known":3,"missing":["backhand","hand"],"of":5},"hand":null,"id":238,"is_doubles_team":false,"name":"Lia Karatancheva","ranking":376,"ranking_movement":"down","ranking_points":167,"tour":"wta"},"p2":{"backhand":null,"birthday":"2001-08-24","country":"jpn","data_completeness":{"known":3,"missing":["backhand","hand"],"of":5},"hand":null,"id":1058,"is_doubles_team":false,"name":"Ikumi Yamazaki","ranking":483,"ranking_movement":"down","ranking_points":120,"tour":null}},"round":"Qualifications","round_code":"Q","scheduled_time":"2026-08-16T16:58:00Z","score":{"age_seconds":3452,"games":[[6,4,6],[2,6,7]],"is_tiebreak":false,"points":["0","0"],"sequence":192,"server":1,"sets":[1,2],"stale":true,"timestamp":"2026-08-16T21:19:31.212228Z"},"status":"live","surface":null,"tour":"itf","tournament":"ITF W75 Kursumlijska Banja 3 Women","tournament_id":null,"updated_at":"2026-08-16T21:18:12.526021Z"},{"event_status":null,"format":"BO3","id":174770,"indoor":false,"is_doubles":false,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":"2002-06-28","country":"ukr","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":48,"is_doubles_team":false,"name":"Marta Kostyuk","ranking":11,"ranking_movement":"same","ranking_points":3830,"tour":"wta"},"p2":{"backhand":null,"birthday":"1998-11-14","country":"usa","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":505,"is_doubles_team":false,"name":"Sofia Kenin","ranking":115,"ranking_movement":"down","ranking_points":684,"tour":"wta"}},"round":"Round of 64","round_code":"R64","scheduled_time":"2026-08-16T19:05:00Z","score":{"age_seconds":70,"games":[[4,6,5],[6,3,4]],"is_tiebreak":false,"points":["30","30"],"sequence":165,"server":1,"sets":[1,1],"stale":false,"timestamp":"2026-08-16T21:20:03.242637Z"},"status":"live","surface":"hard","tour":"wta","tournament":"Cincinnati, USA","tournament_id":"1209","updated_at":"2026-08-16T21:18:12.294465Z"},{"event_status":null,"format":"BO3","id":174667,"indoor":false,"is_doubles":false,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":"2002-09-24","country":"esp","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":282,"is_doubles_team":false,"name":"Jessica Bouzas Maneiro","ranking":90,"ranking_movement":"up","ranking_points":834,"tour":"wta"},"p2":{"backhand":null,"birthday":"1996-05-10","country":"cze","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":984,"is_doubles_team":false,"name":"Katerina Siniakova","ranking":44,"ranking_movement":"up","ranking_points":1170,"tour":"wta"}},"round":"Round of 64","round_code":"R64","scheduled_time":"2026-08-16T20:00:00Z","score":{"age_seconds":79,"games":[[7,1],[6,1]],"is_tiebreak":false,"points":["0","0"],"sequence":72,"server":1,"sets":[1,0],"stale":false,"timestamp":"2026-08-16T21:20:01.804566Z"},"status":"live","surface":"hard","tour":"wta","tournament":"Cincinnati, USA","tournament_id":"1209","updated_at":"2026-08-16T21:18:12.307302Z"},{"event_status":null,"format":"BO3","id":174954,"indoor":false,"is_doubles":false,"is_qualifying":true,"players":{"p1":{"backhand":null,"birthday":"2000-06-23","country":"bra","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":425,"is_doubles_team":false,"name":"Paulo Andre Saraiva Dos Santos","ranking":528,"ranking_movement":"up","ranking_points":81,"tour":"atp"},"p2":{"backhand":null,"birthday":"1996-06-17","country":"usa","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":968,"is_doubles_team":false,"name":"Matt Kuhar","ranking":785,"ranking_movement":"same","ranking_points":39,"tour":"atp"}},"round":"Qualification Round 1","round_code":"Q1","scheduled_time":"2026-08-16T20:30:00Z","score":{"age_seconds":28,"games":[[4],[5]],"is_tiebreak":false,"points":["0","0"],"sequence":13,"server":2,"sets":[0,0],"stale":false,"timestamp":"2026-08-16T21:20:55.692326Z"},"status":"live","surface":"hard","tour":"challenger","tournament":"Kingston, Jamaica, Qualifying","tournament_id":"14177","updated_at":"2026-08-16T21:18:12.466998Z"},{"event_status":null,"format":"BO3","id":174694,"indoor":false,"is_doubles":false,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":"2005-12-02","country":"usa","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"L","id":511,"is_doubles_team":false,"name":"Learner Tien","ranking":19,"ranking_movement":"down","ranking_points":2155,"tour":"atp"},"p2":{"backhand":null,"birthday":null,"country":null,"data_completeness":{"known":0,"missing":["backhand","birthday","country","hand","ranking"],"of":5},"hand":null,"id":32089,"is_doubles_team":false,"name":"S. Baez","ranking":null,"ranking_movement":null,"ranking_points":null,"tour":"atp"}},"round":"Round of 64","round_code":"R64","scheduled_time":"2026-08-16T20:35:00Z","score":{"age_seconds":87,"games":[[6,1],[3,1]],"is_tiebreak":false,"points":["0","0"],"sequence":37,"server":2,"sets":[1,0],"stale":false,"timestamp":"2026-08-16T21:20:02.287500Z"},"status":"live","surface":"hard","tour":"atp","tournament":"Cincinnati, USA","tournament_id":"1210","updated_at":"2026-08-16T21:18:12.263140Z"},{"event_status":null,"format":"BO3","id":174723,"indoor":false,"is_doubles":true,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":null,"country":null,"data_completeness":{"known":null,"missing":[],"note":"doubles team \u2014 per-player profile fields do not apply","of":null},"hand":null,"id":31203,"is_doubles_team":true,"name":"Linda Noskova / Marie Bouzkova","ranking":null,"ranking_movement":null,"ranking_points":null,"tour":"WTA"},"p2":{"backhand":null,"birthday":null,"country":null,"data_completeness":{"known":null,"missing":[],"note":"doubles team \u2014 per-player profile fields do not apply","of":null},"hand":null,"id":32207,"is_doubles_team":true,"name":"Caroline Dolehide / Navarro","ranking":null,"ranking_movement":null,"ranking_points":null,"tour":"WTA"}},"round":"Round of 32","round_code":"R32","scheduled_time":"2026-08-16T20:40:00Z","score":{"age_seconds":64,"games":[[5],[4]],"is_tiebreak":false,"points":["0","0"],"sequence":55,"server":1,"sets":[0,0],"stale":false,"timestamp":"2026-08-16T21:20:00.496475Z"},"status":"live","surface":"hard","tour":"wta","tournament":"Cincinnati, USA, Doubles","tournament_id":"1212","updated_at":"2026-08-16T21:18:12.388839Z"},{"event_status":null,"format":"BO3","id":174686,"indoor":false,"is_doubles":false,"is_qualifying":false,"players":{"p1":{"backhand":2,"birthday":"1994-11-15","country":null,"data_completeness":{"known":4,"missing":["country"],"of":5},"hand":"R","id":45,"is_doubles_team":false,"name":"Ekaterina Alexandrova","ranking":18,"ranking_movement":"down","ranking_points":2506,"tour":"wta"},"p2":{"backhand":null,"birthday":"1998-09-10","country":"rus","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":278,"is_doubles_team":false,"name":"Anna Blinkova","ranking":108,"ranking_movement":"up","ranking_points":749,"tour":"wta"}},"round":"Round of 64","round_code":"R64","scheduled_time":"2026-08-16T20:45:00Z","score":{"age_seconds":69,"games":[[3],[4]],"is_tiebreak":false,"points":["15","15"],"sequence":35,"server":2,"sets":[0,0],"stale":false,"timestamp":"2026-08-16T21:20:03.679119Z"},"status":"live","surface":"hard","tour":"wta","tournament":"Cincinnati, USA","tournament_id":"1209","updated_at":"2026-08-16T21:18:12.329343Z"},{"event_status":null,"format":"BO3","id":174746,"indoor":false,"is_doubles":false,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":"2004-09-26","country":"esp","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":214,"is_doubles_team":false,"name":"Daniel Merida Aguilar","ranking":40,"ranking_movement":"up","ranking_points":1117,"tour":"atp"},"p2":{"backhand":null,"birthday":"1999-06-03","country":"bel","data_completeness":{"known":4,"missing":["backhand"],"of":5},"hand":"R","id":528,"is_doubles_team":false,"name":"Zizou Bergs","ranking":33,"ranking_movement":"up","ranking_points":1365,"tour":"atp"}},"round":"Round of 64","round_code":"R64","scheduled_time":"2026-08-16T20:45:00Z","score":{"age_seconds":9,"games":[[4],[2]],"is_tiebreak":false,"points":["0","0"],"sequence":31,"server":2,"sets":[0,0],"stale":false,"timestamp":"2026-08-16T21:20:55.606752Z"},"status":"live","surface":"hard","tour":"atp","tournament":"Cincinnati, USA","tournament_id":"1210","updated_at":"2026-08-16T21:18:12.278910Z"},{"event_status":null,"format":"BO3","id":174955,"indoor":false,"is_doubles":false,"is_qualifying":true,"players":{"p1":{"backhand":2,"birthday":"1994-07-16","country":"per","data_completeness":{"known":5,"missing":[],"of":5},"hand":"R","id":354,"is_doubles_team":false,"name":"Arklon Cordova Huertas Del Pino","ranking":719,"ranking_movement":"up","ranking_points":47,"tour":"atp"},"p2":{"backhand":null,"birthday":"2004-03-16","country":"jpn","data_completeness":{"known":3,"missing":["backhand","hand"],"of":5},"hand":null,"id":5846,"is_doubles_team":false,"name":"Kenta Miyoshi","ranking":495,"ranking_movement":"down","ranking_points":91,"tour":"atp"}},"round":"Kingston - Semi-finals","round_code":"SF","scheduled_time":"2026-08-16T21:05:00Z","score":{"age_seconds":657,"games":[[0],[1]],"is_tiebreak":false,"points":["40","30"],"sequence":4,"server":1,"sets":[0,0],"stale":true,"timestamp":"2026-08-16T21:10:07.699155Z"},"status":"live","surface":"hard","tour":"challenger","tournament":"Kingston (Jamaica)","tournament_id":"14177","updated_at":"2026-08-16T21:18:17.204664Z"},{"event_status":null,"format":"BO3","id":174728,"indoor":false,"is_doubles":true,"is_qualifying":false,"players":{"p1":{"backhand":null,"birthday":null,"country":null,"data_completeness":{"known":null,"missing":[],"note":"doubles team \u2014 per-player profile fields do not apply","of":null},"hand":null,"id":1827,"is_doubles_team":true,"name":"Danilina / Krunic","ranking":null,"ranking_movement":null,"ranking_points":null,"tour":"WTA"},"p2":{"backhand":null,"birthday":null,"country":null,"data_completeness":{"known":null,"missing":[],"note":"doubles team \u2014 per-player profile fields do not apply","of":null},"hand":null,"id":1844,"is_doubles_team":true,"name":"Clara Tauson / Zongyu Li","ranking":null,"ranking_movement":null,"ranking_points":null,"tour":"WTA"}},"round":"WTA Cincinnati - 1/16-finals","round_code":"R32","scheduled_time":"2026-08-16T21:15:00Z","score":{"age_seconds":110,"games":[[0],[0]],"is_tiebreak":false,"points":["0","0"],"sequence":1,"server":null,"sets":[0,0],"stale":false,"timestamp":"2026-08-16T21:20:04.115790Z"},"status":"live","surface":"hard","tour":"wta","tournament":"Cincinnati","tournament_id":"1212","updated_at":"2026-08-16T21:19:14.902724Z"}],"meta":{"count":10,"has_more":true,"limit":10,"offset":0,"total":11}}

@thyttan

thyttan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Started working for me now. Think there was something weird with my phone and maybe dns. Not sure.

Looks good. I'll merge and if you fix the presentation of the errors please post another PR 👍

@thyttan
thyttan merged commit 5f840b9 into espruino:master Aug 16, 2026
2 checks passed
@bensynapse

Copy link
Copy Markdown
Contributor Author

Thanks for the merge — and for going as far as live-testing it on your own watch and debugging a phone-side DNS gremlin along the way. That's review service well beyond the call.

On the error presentation: agreed, the raw timeout text is unreadable at watch size. I'll put up a follow-up PR that formats error states properly for the screen (short human message, no stack-ish strings).

One heads-up in return: the curl/logcat output you pasted while debugging includes your actual API key in the Authorization header. It's a free-tier key so the blast radius is small, but since this thread is public I'd rotate it — takes a few seconds from the dashboard at livetennisapi.com (same key page you created it on; the old one dies instantly).

thyttan pushed a commit that referenced this pull request Aug 17, 2026
Follow-up to #4316: replace the raw error text (unreadable at watch
size) with short human messages -- No connection, Request timed out,
Check API key, Not connected, Needs Gadgetbridge, Rate limited, Bad
response -- centered and wrapped in the large font, with a small
'tap to retry' hint at the bottom. Unrecognised errors keep a short
one-line detail in the small font so they stay debuggable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thyttan

thyttan commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Re the api-key. Yeah I figured it didn't matter too much in this case. But since you mentioned it I tried to rotate it. I didn't create an account yesterday so I did that now insted. But trying to attach the api-key to the account (created with the same email-address I used before) I get this message: "Couldn't reach the API to verify the key — please try again."

image

@thyttan

thyttan commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Here is web console output when trying to connect account and api-key:

console-export-2026-8-17_10-39-47.log

@thyttan

thyttan commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Ok, now I noticed the confirmation email in my spam folder and now when I visit the dashboard I see the api-key is connected to the account.

@bensynapse

Copy link
Copy Markdown
Contributor Author

Glad it sorted itself — and that's genuinely useful feedback: the confirmation email landing in spam is an onboarding papercut on our side, not yours. I've flagged it internally so we can look at deliverability for the confirmation sender. Thanks for pushing all the way through to a connected key, and for merging the error-presentation PR.

gfwilliams pushed a commit that referenced this pull request Aug 31, 2026
Follow-up to #4316: replace the raw error text (unreadable at watch
size) with short human messages -- No connection, Request timed out,
Check API key, Not connected, Needs Gadgetbridge, Rate limited, Bad
response -- centered and wrapped in the large font, with a small
'tap to retry' hint at the bottom. Unrecognised errors keep a short
one-line detail in the small font so they stay debuggable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants