Calendar on Watch Face

Former Member
Former Member

Hi all!

By hobby, I am a software developer and I recently purchased a Fenix 6. I've been looking into making a watch face that better fits my needs and I have stumbled upon a problem.
Is there no way currently to have a watch face display the next calendar event? When I scroll down on my watch, there is a calendar widget that does exactly that! I have googled all over and visited forms from 4 years ago that said it was not possible. If that still the case? How does the widget have access to the next calendar event and how do I implement that on to a watch face?

Thanks in advance.

  • CIQ doesn't have access to this info.  Native widgets are not written in CIQ, and have access to things that aren't available in CIQ.

  • Hi,

    I know this thread is old, but I wanted to share that it IS possible to display Google Calendar events on a Garmin watch face – just not via the native CIQ Calendar API.

    I developed this with AI assistance (Claude) which helped significantly with the Monkey C syntax and debugging – though we had to reverse-engineer several undocumented behaviors together. The Code works perfectly, but take it with a grain of salt. 

    Here's the approach that works:

    **Architecture:**
    - Background Service fetches a Google OAuth2 access token every 5 minutes using a stored refresh token
    - Then fetches events from Google Calendar REST API (multiple calendars supported)
    - Sorts events by start time, removes expired ones, reduces data to minimum {title, start, end}
    - Passes the list to the foreground via Background.exit()
    - The Watch Face view selects the current/next event every minute from the cached list

    **Key learnings:**
    1. registerForTemporalEvent(Duration) repeats automatically – no need to re-register after Background.exit()
    2. Do NOT call deleteTemporalEvent() in onStop() – it gets called when the background stops too, killing the timer
    3. Pass data via Background.exit(dict), not Application.Storage directly – onBackgroundData() fires immediately
    4. Google returns ISO-8601 with timezone offset (e.g. +02:00) – subtract offset to get UTC for comparison with Time.now().value()
    5. Background.exit() has ~8KB limit – reduce event data to minimum keys

    Tested on fēnix 7X with Connect IQ SDK 9.1.0.

    Happy to share more details if anyone is interested!

    // =============================================================================
    // KalenderUhrApp.mc
    // Main application class for the Google Calendar Watch Face
    //
    // NOTE FOR PUBLIC APPS:
    // This example uses hardcoded OAuth credentials in Background.mc.
    // For a public app you should implement a proper OAuth flow where the user
    // authenticates via Garmin's OAuth mechanism or a companion phone app.
    // Hardcoded credentials mean all users share the same Google API quota
    // and the refresh token can expire or be revoked.
    // See: developer.garmin.com/.../
    // =============================================================================

    function onStart(state as Dictionary?) as Void {
        // Duration repeats automatically - no need to re-register after exit()
        Background.registerForTemporalEvent(new Time.Duration(5 * 60));
    }

    function onStop(state as Dictionary?) as Void {
        // DO NOT call deleteTemporalEvent() here!
        // onStop fires for background process too - kills the timer!
    }

    function onBackgroundData(data as Application.PersistableType) as Void {
        // Use Background.exit(dict) not Application.Storage directly
        // onBackgroundData fires immediately when background finishes
        if (data != null) {
            var dict = data as Dictionary;
            Application.Storage.setValue("calEvents", dict["events"]);
        }
        WatchUi.requestUpdate();
    }

    // =============================================================================
    // Background.mc
    // Background service for fetching Google Calendar events via OAuth2 REST API
    //
    // ARCHITECTURE:
    // 1. Fetch OAuth2 access token using a stored refresh token
    // 2. Fetch events from one or more Google Calendars
    // 3. Merge all events, sort by start time, remove expired events
    // 4. Reduce data to minimum {t=title, s=startSec, e=endSec}
    // 5. Pass reduced event list to foreground via Background.exit()
    //
    // The foreground view then selects the current/next event every minute,
    // giving real-time transitions without waiting for the next background run.
    //
    // NOTE FOR PUBLIC APPS:
    // This example hardcodes OAuth credentials (client_id, client_secret,
    // refresh_token) and calendar IDs directly in the code. This approach
    // works for personal use only. For a public app:
    // - Use Garmin's App Settings to let users enter their own credentials
    // - Or implement a companion phone app for the OAuth flow
    // - Or use a proxy server that handles OAuth on behalf of your users
    // The refresh token can expire (e.g. after 6 months of inactivity) and
    // will need to be renewed. Google also limits concurrent refresh token usage.
    //
    // HOW TO GET YOUR CREDENTIALS:
    // 1. Create a project at console.cloud.google.com
    // 2. Enable Google Calendar API
    // 3. Create OAuth2 credentials (Web Application type for Playground)
    // 4. Add developers.google.com/oauthplayground as redirect URI
    // 5. Use OAuth Playground to generate a refresh token with scope:
    //    www.googleapis.com/.../calendar.readonly
    // 6. Get your calendar ID(s) from Google Calendar settings
    //    (URL-encode the @ sign as %40)
    //
    // DATA SIZE LIMIT:
    // Background.exit() has an ~8KB limit. With many events, reduce data
    // to minimum keys (t/s/e) as done here. For 48h window with 20 events/day
    // this stays well within limits.
    // =============================================================================

    function onTemporalEvent() as Void {
        // Step 1: Get fresh access token
        Communications.makeWebRequest(
            "">oauth2.googleapis.com/token",
            { "client_id" => "...", "client_secret" => "...",
              "refresh_token" => "...", "grant_type" => "refresh_token" },
            { :method => Communications.HTTP_REQUEST_METHOD_POST,
              :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON },
            method(:onTokenResponse)
        );
    }

    function onEventsResponse(code as Number, data as Dictionary or Null) as Void {
        // ... fetch and merge multiple calendars into _allItems ...

        // Sort by start time (Bubble Sort - no stdlib sort in Monkey C)
        for (var i = 0; i < _allItems.size() - 1; i++) {
            for (var j = 0; j < _allItems.size() - 1 - i; j++) {
                var startA = isoToMoment(getTimeStr(_allItems[j], "start"));
                var startB = isoToMoment(getTimeStr(_allItems[j+1], "start"));
                if (startA != null && startB != null && startA.value() > startB.value()) {
                    var tmp = _allItems[j]; _allItems[j] = _allItems[j+1]; _allItems[j+1] = tmp;
                }
            }
        }

        // Remove expired, reduce to minimum data (~70 bytes/event vs ~400 bytes raw)
        var events = [] as Array;
        for (var i = 0; i < _allItems.size(); i++) {
            var endMom   = isoToMoment(getTimeStr(_allItems[i], "end"));
            var startMom = isoToMoment(getTimeStr(_allItems[i], "start"));
            if (endMom != null && endMom.value() > nowSec) {
                events.add({ "t" => _allItems[i]["summary"],
                             "s" => startMom.value(),
                             "e" => endMom.value() });
            }
        }
        Background.exit({"events" => events}); // ~8KB limit!
    }

    // KEY: Google returns +02:00 offset - subtract to get UTC for Time.now() comparison
    private function isoToMoment(iso as String or Null) as Time.Moment or Null {
        // ... parse year/month/day/hour/min ...
        var sign = tz.substring(0, 1).equals("+") ? -1 : 1; // subtract offset!
        // result is pure UTC unix seconds - comparable with Time.now().value()
    }

    // =============================================================================
    // KalenderUhrView.mc
    // Watch Face View for Google Calendar Watch Face
    //
    // DESIGN:
    // - Current event: shown large (2 lines) with countdown timer
    // - Next event: shown small/grey with "in X min" countdown
    // - No event: shows "Frei" (Free)
    //
    // The view reads the cached event list from Application.Storage every minute
    // and selects the current/next event live. This means event transitions
    // happen in real-time without waiting for the background service.
    //
    // LAYOUT (280x280px round display, fēnix 7X):
    //   - Time: top center
    //   - Heart Rate: upper right (at 45° position)
    //   - Calendar event: center
    //   - Temperature: lower left (at 225° position)
    //   - Body Battery: bottom center
    //   - Battery life: lower right (at 135° position)
    // =============================================================================

    private function drawCenter(dc as Dc) as Void {
        var events = Application.Storage.getValue("calEvents") as Array or Null;
        var nowSec = Time.now().value();

        // Select current/next event live every minute from cached list
        // → real-time transitions without waiting for background update
        var chosen = null; var isCurrent = false;
        for (var i = 0; i < events.size(); i++) {
            var s = events[i]["s"]; var e = events[i]["e"];
            if (s <= nowSec && e > nowSec) { chosen = events[i]; isCurrent = true; break; }
        }
        if (chosen == null) { /* find next upcoming event */ }

        // Display current or next event...
    }