Skip to content

feat: health stats grid as an alternative to the calendar - #174

Open
unki wants to merge 5 commits into
mattrossman:mainfrom
unki:feat/health-stats-view
Open

unki wants to merge 5 commits into
mattrossman:mainfrom
unki:feat/health-stats-view

Conversation

@unki

@unki unki commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Really enjoying this watchface on my PT2. Personally, I don't get much use out of the calendar, but I'd love to see some health metrics in its place. Inspired by dataload, this PR adds an option to replace the calendar with a metrics grid, with a configurable choice of data points (steps, heart rate, distance, etc.).

Not sure if there is interest in such bigger change, as it adds quite a bit that wasn't there before: new settings, infrastructure to retrieve the data points, and the grid itself. It also extracts a few existing pieces, like the battery indicator and the status icons, so the grid can reuse them. The calendar keeps using the same code and looks unchanged.

Summary

  • Opt-in health stats grid that replaces the 3-week calendar in the top band
  • Configurable tiles: steps, distance, heart rate, activity and more
  • Calendar stays the default
  • Enable via Settings → Top band → Show in top band → Health stats grid

Screenshots

basalt emery
2026-09-13T19-30-53-basalt 2026-09-13T19-31-00-emery

Features

  • Grid size
    • emery: 3×2 tiles
    • basalt, diorite, flint: 2×2 tiles
    • aplite: not available (no health API), settings section hidden
  • Metrics per slot: Activity, Battery, Calories, Date, Distance, Heart rate,
    Steps, Sunrise, Sunset, Temperature, UV index, Weekday, Empty
  • Indicator bars (emery)
    • Calories: vs. 500 kcal
    • Activity: % of the user's typical activity by this time of day
    • UV index, battery level
  • Heart rate graph (emery): last hour as a thin line behind the BPM value
  • Status icons in the grid's top row
    • Quiet time / bluetooth left, battery right
    • Aligned with the tile titles; titles move aside only as far as needed
    • Battery gauge and tile update immediately on battery changes
    • Disconnect vibration works in both modes
  • Distance unit follows the Pebble app's Health setting, falling back to the
    temperature unit
  • Settings only offer what the watch supports
    • Metrics the watch can't supply are hidden (e.g. heart rate without sensor)
    • Only as many slot pickers as the watch displays
    • Slots numbered left to right, top to bottom, with the watch's grid size shown (2x2 / 3x2)
    • Calendar settings greyed out while the stats grid is selected (values kept)
  • Missing or stale data shows --

Implementation

  • New stats grid layer and metrics module on the watch, slot settings in Clay
  • Refactor: battery gauge, status icons and hatch fill shared between calendar
    and grid (calendar renders pixel-identical)
  • Heart rate history is battery-friendly
    • Only reads what the firmware already logs, no extra sensor sampling
    • Read at most every 5 min, cached in between
  • UV index taken from the existing weather response, no extra requests
  • Existing persistence keys untouched, new ones appended
  • Settings saved by older versions upgrade safely: the new fields start at their defaults
  • Adds the health capability: the Pebble app asks users for health permission
  • Integer-only math (see perf: reduce aplite heap usage by avoiding software math #163)
  • New stats fixture for emulator screenshots

mattrossman added a commit that referenced this pull request Sep 20, 2026
Add Open-Meteo as a third opt-in provider with their official forecast
API.
Requests current temperature and UV (for #174) plus 24(+2) hourly
temperature, precipitation probability.
Sun events, coordinates and city name reuse the base provider.

Not sure if I did that with the supabase migration correct though :)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Matt Rossman <22670878+mattrossman@users.noreply.github.com>
unki and others added 4 commits September 30, 2026 20:36
Move the night hatch helpers out of forecast_layer.c into appendix/draw,
the battery gauge out of battery_layer.c into appendix/battery_indicator,
and the quiet-time/bluetooth icons out of calendar_status_layer.c into
appendix/status_icons, so another top-band view can draw them too.

The connection service subscription moves from calendar_status_layer to
main_window, keeping the disconnect vibration independent of which layer
is visible. Rendering is unchanged: calendar-mode fixture screenshots are
pixel-identical on aplite, basalt, diorite, emery and flint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6abcjuotEL5NKg6nVavm2
Add a "top band" setting that swaps the calendar and its status row for a
grid of configurable stat tiles (3x2 on emery, 2x2 on 144px watches):
steps, distance, active calories, activity vs. typical, heart rate, UV
index, battery, current temperature, sunrise, sunset, date and weekday.
Calories, activity, UV and battery draw an indicator bar. aplite has no
health API and keeps the calendar only.

- The watch reports which metrics it can supply, so the settings page only
  offers real options (e.g. heart rate only with a sensor).
- Bluetooth/quiet-time icons and the battery gauge sit in the grid's top
  row, aligned with the tile titles; titles stay centred on their tile and
  only move aside as far as an icon (or the charging bolt) requires.
- Distance follows the unit picked in the Pebble app's Health settings,
  falling back to the temperature unit when none was picked.
- The UV index comes from the already-fetched weather response.
- Fixtures gain watch.health and weather.uvIndex for emulator screenshots,
  plus a new fixtures/stats.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6abcjuotEL5NKg6nVavm2
Draw the last hour of heart rate as a thin dark-red line behind the
HEART tile's title and value on emery, like dataload2 does.

The history comes from health_service_get_minute_history, i.e. only what
the firmware already logs: no sample-period request, no health event
subscription. The 60 minute records are read at most every 5 minutes, only
while the grid is drawn with a heart rate tile, folded into 30 averaged
points and cached; the raw records live on the heap for that call only.
Gaps between readings are bridged, and the vertical scale spans at least
20 bpm so a steady resting pulse stays a calm line.

Fixtures gain an optional watch.health.heart_rate_history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6abcjuotEL5NKg6nVavm2
The stats grid can show a UV index tile, but the Open-Meteo provider
never set uvIndex, so that tile stayed unknown for Open-Meteo users.

Request current.uv_index and map it in withProviderData(). The value is
deliberately left out of the field validation: models and regions that
omit uv_index must still yield a usable forecast, and null (unknown) is
kept distinct from 0, which is a real UV reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@unki
unki force-pushed the feat/health-stats-view branch from 4dbd78d to b9ecb85 Compare September 30, 2026 18:40
With the health stats grid replacing the calendar, the watchface no
longer showed the date anywhere. Add a "Date next to time" setting
(off / left / right) that draws the weekday and the day and month as a
two-line block beside the time, centered together with it.

The second line follows the watch language: "Oct 18th" in English,
"18. Okt" in German, "18 oct" otherwise. setlocale() is now called at
startup so strftime names are localized, which also localizes the
calendar month label and the stats Weekday tile.

Emery only: on 144px screens the date fit beside the time with Bitham
alone, in a barely readable font. Next to emery's 60px Leco there is no
room either, so the setting is greyed out while Leco is selected and the
watch ignores it for Leco. AM/PM switches the date from Gothic 18 bold
to Gothic 14 so it still fits.

The setting is appended to Config (older blobs default to off) and sent
as an optional CLAY_TIME_DATE tuple, so older phone bundles still
deliver their settings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant