Custom generic controls app for Garmin watch


For a couple of years now I’ve had a Garmin Venu 2 plus watch. mostly for tracking my runs, health stuff etc.. I like the Garmin watch, I love the battery life, but obviously it’s a little less of a rich ecosystem of apps.

However they had an SDK and it’s not too hard to make your own. One of the first things I developed for it was a custom watch face.

A watch face is all well and good, but what about actually making use of this computer on your wrist with some custom applications? I had this idea that what I wanted was a dynamic application that exposes controls for a collection of systems in my house. The most obvious being Home assistant which has a app for my phone, but there is no Garmin app.

But thinking flexibly, my idea was to have an app that is just a framework, it calls out to an external service to get tools which menus and controls it should expose. and each control is just another service endpoint. That way I can write the watch app once, and then extend what it presents by changing a back end service.

In the Garmin connect SDK that amounts to 4 pretty small classes.

1) HomeControlApp.mc – this is the entry class, it basically just wires together a View and a Delegate

2) HomeControlDelegate.mc – Responsible for calling out to my backend service, and storing the JSON response. It co-ordinates building the menu view and calling updates on the view

3) HomeControlView.mc – pretty much just call the update functions

4) Menu builder.mc – The main logic for taking the JSON document and turn it into a layer of the menu system. When the user clicks on something which is a menu, we just get passed the sub-fragment of the JSON that represents that layer

At the moment the code supports 3 types of elements in the JSON document, a menu, which just contains another list of items.  A Toggle, which is represented with the menu toggle type which has an active/inactive state etc, these render nicely as little switches. And an action – these are really one time trigger events, in home assistant triggering a scene is like this.

I may want more types in time. but for now this is plenty.

Each toggle and action has an attribute list, and in the attributes is the URL end point to call to trigger it. So with the base url of my backend service plus that url, the Garmin watch app just does a post to that url to fire that action. It automatically assumes a toggle has worked, so no need to refresh the menu state.

In theory the only configuration the watch app requires is the base url of the service. it assumes a call to /controls will return a JSON payload in a valid format.

This is an example of the payload I’m getting right now:

{
"items":
    [
        {
        "items":
            [
                {
                "items":
                    [
                        {
                        "attributes":
                            {
                            "disabled_text": "Off",
                            "enabled_text": "On",
                            "state": false,
                            "url": "/toggle/light.danstudylight"
                            },
                        "title": "DanstudyLight",
                        "type": "toggle"
                        },
                        {
                        "attributes":
                            {
                            "disabled_text": "Off",
                            "enabled_text": "On",
                            "state": false,
                            "url": "/toggle/light.garagelights"
                            },
                        "title": "GarageLights",
                        "type": "toggle"
                        }
                    ],
                "title": "Lights",
                "type": "menu"
                },
                {
                "items":
                    [
                        {
                        "attributes":
                            {
                            "disabled_text": "Off",
                            "enabled_text": "On",
                            "state": false,
                            "url": "/toggle/switch.en_suite_child_lock"
                            },
                        "title": "En Suite Child lock",
                        "type": "toggle"
                        },
                        {
                        "attributes":
                            {
                            "disabled_text": "Off",
                            "enabled_text": "On",
                            "state": false,
                            "url": "/toggle/switch.smart_plug_child_lock"
                            },
                        "title": "Smart Plug Child lock",
                        "type": "toggle"
                        }
                    ],
                "title": "Switches",
                "type": "menu"
                },
                {
                "items":
                    [
                        {
                        "attributes":
                            {
                            "Header": "Iplayer",
                            "SubHeader": "Activate",
                            "url": "/scene/scene.iplayer"
                            },
                        "title": "Iplayer",
                        "type": "action"
                        },
                        {
                        "attributes":
                            {
                            "Header": "Netflix",
                            "SubHeader": "Activate",
                            "url": "/scene/scene.netflix"
                            },
                        "title": "Netflix",
                        "type": "action"
                        },
                        {
                        "attributes":
                            {
                            "Header": "Prime",
                            "SubHeader": "Activate",
                            "url": "/scene/scene.prime"
                            },
                        "title": "Prime",
                        "type": "action"
                        }
                    ],
                "title": "Scenes",
                "type": "menu"
                }
            ],
        "title": "Home Assistant",
        "type": "menu"
        },
        {
        "items":
            [
                {
                "items":
                    [
                    ],
                "title": "Movement",
                "type": "menu"
                }
            ],
        "title": "RoboCamera",
        "type": "menu"
        }
    ],
"title": "Main Menu"
}

And this is what that looks like on the watch

To be honest, I’d guess that the vast majority of things I’m going to want to expose are coming from home assistant since I”m likely to integrate with that first. However for really custom stuff like my robot cameraman and my electronic lead screw project, there is no real point in exposing them to home assistant but it is useful to have some of those controls on my wrist.

I also have the idea that I might want to implement a context aware backend, such that depending on the time of day, time of week etc, it presents a menu that is only the most used actions. Or possibly just prioritises those to a favourites list.

One little gotcha in this setup is the requirements imposed on the backend. Garmin in their wisdom ‘protect’ users by preventing the SDK being able to call http end points. you can only talk to HTTPS so to use my own api I suddenly had to present it as a secure site, even though this is all just running on my own internal network.

The first time I came across this it took much longer than I wanted, but honestly it led me in a direction that has been useful for other things since. I now run a SWAG docker container that has nginx inside, I already own the domain that this website is on, and I could direct a sub-domain to connect back to my home connection. Making use of another docker container to poll for my external IP address and dynamically update the route53 records for my domain, I can point traffic towards that swag container and from there to any service I run in my house. anything I want exposed externally just needs external DNS and anything internal only I expose in my local network only (I’m running open wrt with home  guard DNS)

The SWAG container can obtain a wildcard certificate from Let’s encrypt that’s good for anything I want to run on my home network, and now I can expose my home control API over https and have the watch app call it.


Leave a Reply

Only people in my network can comment.