diff --git a/.github/restyled.yml b/.github/restyled.yml index c4eda48a..f26d1c80 100644 --- a/.github/restyled.yml +++ b/.github/restyled.yml @@ -1,3 +1,5 @@ exclude: - '**/*.md' - '**/pnpm-lock.yaml' + - 'manifest.xml' + - 'manifest-widget.xml' diff --git a/BatteryReporting.md b/BackgroundService.md similarity index 60% rename from BatteryReporting.md rename to BackgroundService.md index cdb6024c..9e745561 100644 --- a/BatteryReporting.md +++ b/BackgroundService.md @@ -1,9 +1,52 @@ [Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | Battery Reporting | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md) -# Battery Reporting +# Background Service + +The background service can report the following statuses from your device to your Home Assistant: + +- Battery Level with charging status. +- Location and location accuracy. +- Activity information, but only if your device supports API level 3.2.0. If your device does not support this API level, this information is simply omitted. How do you know? Easiest way is to see if the data is reported. + +## Limits + +The values are merely samples of your device's current status. They are sent by a single background service at the repetition frequency you chose in the settings. The samples are sent at that one rate only, they _do not vary_ for example on in activity, on charge, time of day. You get one refresh interval and that is it. If you want to change the refresh interval, you change your settings. We do appreciate that may not be what you would ideally like to trigger actions on Home Assistant. Messing with the repeat interval of the background service requires more code, more settings and more complexity. That means older devices using widgets would have to be taken out of support to achieve it. + +**Please do not ask for these to be made 'events'.** Garmin's [Connect IQ background service](https://developer.garmin.com/connect-iq/api-docs/Toybox/System/ServiceDelegate.html) is limited in that while it does provide an `onActivityCompleted()` method, it does not provide an `onActivityStarted()` method, so you would not have the complete activity life cycle anyway. So we're keeping this implementation simple, you just get a sampling at one refresh rate. This probably limits you to updating a status on a Home Assistant Dashboard only. + +## Battery Reporting From version 2.1 the application includes a background service to report the current device battery level and charging status back to Home Assistant. This is a feature that Garmin omitted to include with the Bluetooth connection. +## Location Reporting + +From version 2.6 the application includes reporting your location. The location data reported includes: + +- Location (latitude and longitude) +- Location accuracy +- Speed +- Direction +- Altitude + +You get whatever your device provides at the moment, i.e. at the accuracy the device currently provides. If your watch is not calibrated you get poor data. It might mean that you get more accurate location data when you are in a location tracking activity (i.e. not swimming pool lengths). The device [indicates an accuracy](https://developer.garmin.com/connect-iq/api-docs/Toybox/Position.html#Quality-module) in units of: + +- `Position.QUALITY_NOT_AVAILABLE` - No update provided +- `Position.QUALITY_LAST_KNOWN` - No update provided +- `Position.QUALITY_POOR` - We translate that to 500 m arbitrarily +- `Position.QUALITY_USABLE` - We translate that to 100 m arbitrarily +- `Position.QUALITY_GOOD` - We translate that to 10 m arbitrarily + +**You cannot rely on the radius of the circle of accuracy in any resulting maps as any meaningful indication of error.** + +## Activity Reporting + +From version 2.6 the application includes reporting your activity. The activity data includes: + +- Activity - This is an integer as defined by [Toybox.Activity `SPORT`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module) +- Sub-activity - This is an integer as defined by [Toybox.Activity `SUB_SPORT`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#SubSport-module) + +The application only provides the integers without translation. When using the values in Home Assistant, you will need to provide you own mapping from the `Activity` enumerated type to the human readable text. As developers of the application we are pushing this translation to the server to keep the Garmin application code 'lean'. You will also need to add to both the list of activities (sports) and sub-activities (sub-sports) an interpretation of integer `-1` for no activity/sub-activity at present. + ## Start Reporting The main drawback of this solution is that the Garmin application must be run once with the feature enabled in the settings before reporting will start. Reporting continues after you have exited the application. This is a limit we cannot code around. diff --git a/HISTORY.md b/HISTORY.md index aaa9133d..5acadbe1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,4 @@ -[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Battery Reporting](BatteryReporting.md) | [Trouble Shooting](TroubleShooting.md) | Version History +[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | Version History # Version History @@ -13,8 +13,9 @@ | 1.6 | Added a user configurable 'timeout' in seconds so that when no action is taken the application automatically closes, stopping the continuous polling for changes of status and hence saving the drain on the battery. This can be disabled with timeout=0. | | 1.7 | Added timeout to confirmation views so that when used for security devices it does not linger when left unconfirmed. Thanks to [Jan Schneider](https://github.com/j-a-n) for the contribution. Known bug for devices not supporting [`WatchUi.getCurrentView()`](https://developer.garmin.com/connect-iq/api-docs/Toybox/WatchUi.html#getCurrentView-instance_function) API call which is only available on API Level 3.4.0, e.g. Vivoactive 4S. | | 2.0 | A significant code base change to enable both a 'widget' version for older devices, e.g. Venu (1), and an application with a glance, e.g. Venu2. These two versions must now be distributed under separate application IDs, but they have the same code base. A further 20 more devices are now supported, the settings have been internationalised, and there's a bug fix for older devices when trying to display a helpful error message but instead the application crashed. This version has come from a significant collaboration with [Someone0nEarth](https://github.com/Someone0nEarth). | -| 2.1 | Deployment of an idea to provide Home Assistant with access to the watch battery level. Using this requires [significant setup](BatteryReporting.md) on the Home Assistant configuration and hence is detailed separately. Due to this, the default state for this battery option is _off_. Changed the application settings user interface to be more intuitive, and hence amended the way settings are managed in the background. | +| 2.1 | Deployment of an idea to provide Home Assistant with access to the watch battery level. Using this requires [significant setup](BackgroundService.md) on the Home Assistant configuration and hence is detailed separately. Due to this, the default state for this battery option is _off_. Changed the application settings user interface to be more intuitive, and hence amended the way settings are managed in the background. | | 2.2 | Adds a feature to cache the menu configuration and save the time taken for an HTTP request to fetch it. You as the user are responsible for managing the cache by clearing it when you update your configuration. Improvement to widget root display updates. Bug fix for battery level reporting when in the glance carousel. Fixed an uninternationalised string, "Execute". Unfixed issue with battery level updates when the user is not an administrator. | | 2.3 | Fix for battery level updates where previously the function only worked for administrator accounts. The new solution is based on Webhooks and is simpler to implement on Home Assistant. Language support fix where an automatic translation produced an inappropriate word, possibly in more than one language. | | 2.4 | Sensor status reporting via Home Assistant 'templates'. This provides a generalised way of viewing the status of any entity as long as the result can be rendered as text, e.g. 'uncovered', 'open', '76%', '21 °C'. Removal of the menu style option. The original style was kept after the introduction of the icon style solely to keep the code for a possible re-use for sensor statuses. This version delivers that new feature, hence the style option has been removed. The new JSON configuration file format allows for the old style to be replicated if you are desperate! Added a feature to provide parameters to actions (`tap` or `template`). Added a feature to confirm `toggle` menu items. | | 2.5 | A small memory efficiency of about 1kB by removing `RezStrings.mc`. This will aid widgets on old watches that only have 60kB available to an application and are using about 45kB before the menu is fetched, hence 1kB is more significant to those devices. | +| 2.6 | Added more information reporting to the background service, in addition to the device battery level and charging status, we now include location, location accuracy, and (if supported by your device) the activity information. Note the updates are sent periodically and are not event driven. | \ No newline at end of file diff --git a/README.md b/README.md index acd578ce..43263dea 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Home | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Battery Reporting](BatteryReporting.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md) +Home | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md) # GarminHomeAssistant @@ -249,7 +249,9 @@ Home Assistant will inevitably change the state of devices you are also controll The per toggle item delay is caused by a queue of responses to web requests. The responses fill up a buffer and in early testing we observed [`Communications.BLE_QUEUE_FULL`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html) response codes. For a Venu 2 Garmin watch an API call delay of 600 ms was found to be sustainable (500 ms was still too fast). The code now chains a sequence of updates, so as one finishes it invokes the next item's update. **The more items requiring a status update that you pack into your dashboard, the slower each individual item will be updated!** -The thinking here is that the watch application will only ever be open briefly not persistently, so the delay in picking up state changes won't be observed often for any race condition between two controllers. As a consequence of this update mechanism, if you request changes too quickly you will be notified that your device cannot keep up with the rate of API responses and you will have to dismiss the error in order to continue. The is a _feature not a bug_! +The thinking here is that the watch application will only ever be open briefly not persistently, so the delay in picking up state changes won't be observed often for any race condition between two controllers. As a consequence of this update mechanism, if you request changes too quickly you will be notified that your device cannot keep up with the rate of API responses and you will have to dismiss the error in order to continue. This is a _feature not a bug_! If the application reduces the rate of "round robin" status update requests it becomes less responsive to external changes. + +To prevent excessive battery usage, set the application timeout in the settings. This will prevent you from leaving the application open and forgotten when not being used, and the polling mechanism will then cease, saving battery life. Again, the thinking here is that the watch application will only ever be open briefly not persistently, and hence not be a constant source of battery usage unless the [background service](BackgroundService.md) for sending any watch status is used aggressively fast. ## Changes to the (JSON) Dashboard Definition diff --git a/TroubleShooting.md b/TroubleShooting.md index 964d4ae4..abf1f433 100644 --- a/TroubleShooting.md +++ b/TroubleShooting.md @@ -1,4 +1,4 @@ -[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Battery Reporting](BatteryReporting.md) | Trouble Shooting | [Version History](HISTORY.md) +[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | Trouble Shooting | [Version History](HISTORY.md) # Troubleshooting Guides diff --git a/examples/Actions.md b/examples/Actions.md index a6c08fc8..68190db1 100644 --- a/examples/Actions.md +++ b/examples/Actions.md @@ -1,4 +1,4 @@ -[Home](../README.md) | [Switches](Switches.md) | Actions | [Templates](Templates.md) | [Battery Reporting](../BatteryReporting.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) +[Home](../README.md) | [Switches](Switches.md) | Actions | [Templates](Templates.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) # Actions diff --git a/examples/Switches.md b/examples/Switches.md index 1696b190..fe7e22a6 100644 --- a/examples/Switches.md +++ b/examples/Switches.md @@ -1,4 +1,4 @@ -[Home](../README.md) | Switches | [Actions](Actions.md) | [Templates](Templates.md) | [Battery Reporting](../BatteryReporting.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) +[Home](../README.md) | Switches | [Actions](Actions.md) | [Templates](Templates.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) # Switches diff --git a/examples/Templates.md b/examples/Templates.md index 481bea84..ed5fdc50 100644 --- a/examples/Templates.md +++ b/examples/Templates.md @@ -1,11 +1,14 @@ -[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | Templates | [Battery Reporting](../BatteryReporting.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) +[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | Templates | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md) # Templates -In order to provide the most functionality possible the content of the menu item comes from a user-defined template (i.e. you generate your own text). This allows you to do some pretty cool things. It also makes the config a bit more complicated. This page will help you understand how to use templates. +In order to provide the most functionality possible the content of the menu item comes from a user-defined template (i.e. you generate your own text). This allows you to do some pretty cool things. It also makes the configuration a bit more complicated. This page will help you understand how to use templates. - In this file anything between `<` and `>` is a placeholder. Replace it with the appropriate value. -- Anything between `{{` and `}}` is a template. Templates are used to dynamically insert values into the content. For more info see [the docs](https://www.home-assistant.io/docs/configuration/templating/). +- [Jinga2](https://palletsprojects.com/p/jinja/) syntax is used by Home Assistant [Templates](https://www.home-assistant.io/docs/configuration/templating/). Templates are used to dynamically insert values into the content. The syntax includes: + - `{%` ... `%}` for Statements + - `{{` ... `}}` for Expressions to print to the template output + - `{#` ... `#}` for Comments not included in the template output ## States @@ -45,6 +48,16 @@ The first two keep to the simple proposal above. The last combines them into a s } ``` +In order to keep the formatting of floating point numbers under control, you might also like to include a format string as follows. `states()` seems to return a `string` that needs converting to a `float` before the `format()` call can manage the conversion to the required number fo decimal places. + +```json +{ + "name": "Hallway", + "type": "template", + "content": "T:{{ '%.1f'|format(states('sensor.hallway_temperature')|float) }}°C, H:{{ '%.1f'|format(states('sensor.hallway_humidity')|float) }}%" +}, +``` + ## Conditionals Anything between `{%` and `%}` is a directive (`if`, `else`, `elif`, `endif`, etc.). Conditionals are used to dynamically change the content based on the state of the entity. diff --git a/images/GarminHomeAssistantSettings.png b/images/GarminHomeAssistantSettings.png index 2fbdf200..a8fb34c3 100644 Binary files a/images/GarminHomeAssistantSettings.png and b/images/GarminHomeAssistantSettings.png differ diff --git a/manifest-widget.xml b/manifest-widget.xml index 227f8053..cfa41e16 100644 --- a/manifest-widget.xml +++ b/manifest-widget.xml @@ -34,115 +34,116 @@ "Monkey C: Edit Products" - Lets you add or remove any product --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + - + \ No newline at end of file diff --git a/manifest.xml b/manifest.xml index 1d58e488..13d29bce 100644 --- a/manifest.xml +++ b/manifest.xml @@ -40,115 +40,116 @@ "Monkey C: Edit Products" - Lets you add or remove any product --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + - + \ No newline at end of file diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index e6967927..12ddf3c9 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -45,21 +45,14 @@ URL for HomeAssistant API. URL for menu configuration (JSON). Should the application cache the menu configuration? - Should the application clear the existing cache next time it is - started? - Timeout in seconds. Exit the application after this period of - inactivity to save the device battery. - After this time (in seconds), a confirmation dialog for an - action is automatically closed and the action is cancelled. Set to 0 to disable the timeout. + Should the application clear the existing cache next time it is started? + Timeout in seconds. Exit the application after this period of inactivity to save the device battery. + After this time (in seconds), a confirmation dialog for an action is automatically closed and the action is cancelled. Set to 0 to disable the timeout. Left (off) or Right (on) Menu Alignment. Left to right Right to Left - (Widget only) Automatically start the application from the widget - without waiting for a tap. - Enable the background service to send the clock battery - level to Home Assistant. - The refresh rate (in minutes) at which the background - service should repeat sending the battery level. - (Read only) The Webhook ID created by the watch for battery level updates. - You might require this for debugging. + (Widget only) Automatically start the application from the widget without waiting for a tap. + Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant. + The refresh rate (in minutes) at which the background service should repeat sending data. + (Read only) The Webhook ID created by the device for background service updates. You might require this for debugging. diff --git a/source/BackgroundServiceDelegate.mc b/source/BackgroundServiceDelegate.mc index a1746cb9..b76e38a3 100644 --- a/source/BackgroundServiceDelegate.mc +++ b/source/BackgroundServiceDelegate.mc @@ -38,28 +38,98 @@ class BackgroundServiceDelegate extends System.ServiceDelegate { } function onTemporalEvent() as Void { - if (! System.getDeviceSettings().phoneConnected) { + if (!System.getDeviceSettings().phoneConnected) { // System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call."); - } else if (! System.getDeviceSettings().connectionAvailable) { + } else if (!System.getDeviceSettings().connectionAvailable) { // System.println("BackgroundServiceDelegate onTemporalEvent(): No Internet connection, skipping API call."); } else { + // System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call."); + var position = Position.getInfo(); + // System.println("BackgroundServiceDelegate onTemporalEvent(): gps: " + position.position.toDegrees()); + // System.println("BackgroundServiceDelegate onTemporalEvent(): speed: " + position.speed); + // System.println("BackgroundServiceDelegate onTemporalEvent(): course: " + position.heading + "rad (" + (position.heading * 180 / Math.PI) + "°)"); + // System.println("BackgroundServiceDelegate onTemporalEvent(): altitude: " + position.altitude); + // System.println("BackgroundServiceDelegate onTemporalEvent(): battery: " + System.getSystemStats().battery); + // System.println("BackgroundServiceDelegate onTemporalEvent(): charging: " + System.getSystemStats().charging); + // System.println("BackgroundServiceDelegate onTemporalEvent(): activity: " + Activity.getProfileInfo().name); + // Don't use Settings.* here as the object lasts < 30 secs and is recreated each time the background service is run + + if (position.accuracy != Position.QUALITY_NOT_AVAILABLE && position.accuracy != Position.QUALITY_LAST_KNOWN) { + var accuracy = 0; + switch (position.accuracy) { + case Position.QUALITY_POOR: + accuracy = 500; + break; + case Position.QUALITY_USABLE: + accuracy = 100; + break; + case Position.QUALITY_GOOD: + accuracy = 10; + break; + } + Communications.makeWebRequest( + (Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String), + { + "type" => "update_location", + "data" => { + "gps" => position.position.toDegrees(), + "gps_accuracy" => accuracy, + "speed" => Math.round(position.speed), + "course" => Math.round(position.heading * 180 / Math.PI), + "altitude" => Math.round(position.altitude), + } + }, + { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }, + method(:onReturnBatteryUpdate) + ); + } + var data = [ + { + "state" => System.getSystemStats().battery, + "type" => "sensor", + "unique_id" => "battery_level" + }, + { + "state" => System.getSystemStats().charging, + "type" => "binary_sensor", + "unique_id" => "battery_is_charging" + } + ]; + if ((Activity has :getActivityInfo) and (Activity has :getProfileInfo)) { + var activity = Activity.getProfileInfo().sport; + var sub_activity = Activity.getProfileInfo().subSport; + // We need to check if we are actually tracking any activity as the enumerated type does not include "No Sport". + if ((Activity.getActivityInfo() != null) and + ((Activity.getActivityInfo().elapsedTime == null) or + (Activity.getActivityInfo().elapsedTime == 0))) { + // Indicate no activity with -1, not part of Garmin's activity codes. + // https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module + activity = -1; + sub_activity = -1; + } + data.add({ + "state" => activity, + "type" => "sensor", + "unique_id" => "activity" + }); + data.add({ + "state" => sub_activity, + "type" => "sensor", + "unique_id" => "sub_activity" + }); + } Communications.makeWebRequest( (Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String), { "type" => "update_sensor_states", - "data" => [ - { - "state" => System.getSystemStats().battery, - "type" => "sensor", - "unique_id" => "battery_level" - }, - { - "state" => System.getSystemStats().charging, - "type" => "binary_sensor", - "unique_id" => "battery_is_charging" - } - ] + "data" => data }, { :method => Communications.HTTP_REQUEST_METHOD_POST, diff --git a/source/WebhookManager.mc b/source/WebhookManager.mc index 3e38f729..9c696438 100644 --- a/source/WebhookManager.mc +++ b/source/WebhookManager.mc @@ -62,10 +62,12 @@ class WebhookManager { ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String); break; + case 200: case 201: var id = data.get("webhook_id") as Lang.String or Null; if (id != null) { Settings.setWebhookId(id); + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering first sensor: Battery Level"); registerWebhookSensor({ "device_class" => "battery", "name" => "Battery Level", @@ -76,16 +78,7 @@ class WebhookManager { "state_class" => "measurement", "entity_category" => "diagnostic", "disabled" => false - }); - registerWebhookSensor({ - "device_class" => "battery_charging", - "name" => "Battery is Charging", - "state" => System.getSystemStats().charging, - "type" => "binary_sensor", - "unique_id" => "battery_is_charging", - "entity_category" => "diagnostic", - "disabled" => false - }); + }, 0); } else { // System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data."); Settings.unsetIsBatteryLevelEnabled(); @@ -102,18 +95,20 @@ class WebhookManager { function requestWebhookId() { // System.println("WebhookManager requestWebhookId(): Requesting webhook id"); + var deviceSettings = System.getDeviceSettings(); Communications.makeWebRequest( Settings.getApiUrl() + "/mobile_app/registrations", { - "device_id" => System.getDeviceSettings().uniqueIdentifier, + "device_id" => deviceSettings.uniqueIdentifier, "app_id" => "garmin_home_assistant", "app_name" => WatchUi.loadResource($.Rez.Strings.AppName) as Lang.String, "app_version" => "", - "device_name" => "Garmin Watch", + "device_name" => "Garmin Device", "manufacturer" => "Garmin", - "model" => "", + // An unhelpful part number that can be translated to a familiar model name. + "model" => deviceSettings.partNumber, "os_name" => "", - "os_version" => Lang.format("$1$.$2$", System.getDeviceSettings().firmwareVersion), + "os_version" => Lang.format("$1$.$2$", deviceSettings.firmwareVersion), "supports_encryption" => false, "app_data" => {} }, @@ -129,7 +124,7 @@ class WebhookManager { ); } - function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, step as Lang.Number) as Void { switch (responseCode) { case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_CONNECTION_UNAVAILABLE: @@ -155,6 +150,7 @@ class WebhookManager { Settings.unsetWebhookId(); // Ignore and see if we can carry on break; + case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE: // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned."); Settings.unsetWebhookId(); @@ -169,12 +165,67 @@ class WebhookManager { ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String); break; + case 200: case 201: - if ((data.get("success") as Lang.Boolean or Null) != true) { - // When uncommenting, invert the condition above. - // System.println("WebhookManager onReturnRegisterWebhookSensor(): Success"); - // } else { - // System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure"); + if ((data.get("success") as Lang.Boolean or Null) != false) { + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Success"); + switch (step) { + case 0: + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Battery is Charging"); + registerWebhookSensor({ + "device_class" => "battery_charging", + "name" => "Battery is Charging", + "state" => System.getSystemStats().charging, + "type" => "binary_sensor", + "unique_id" => "battery_is_charging", + "entity_category" => "diagnostic", + "disabled" => false + }, 1); + break; + case 1: + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity"); + if (Activity has :getProfileInfo) { + var activity = Activity.getProfileInfo().sport; + if ((Activity.getActivityInfo() != null) and + ((Activity.getActivityInfo().elapsedTime == null) or + (Activity.getActivityInfo().elapsedTime == 0))) { + // Indicate no activity with -1, not part of Garmin's activity codes. + // https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module + activity = -1; + } + registerWebhookSensor({ + "name" => "Activity", + "state" => activity, + "type" => "sensor", + "unique_id" => "activity", + "disabled" => false + }, 2); + break; + } + case 2: + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity"); + if (Activity has :getProfileInfo) { + var sub_activity = Activity.getProfileInfo().subSport; + if ((Activity.getActivityInfo() != null) and + ((Activity.getActivityInfo().elapsedTime == null) or + (Activity.getActivityInfo().elapsedTime == 0))) { + // Indicate no activity with -1, not part of Garmin's activity codes. + // https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module + sub_activity = -1; + } + registerWebhookSensor({ + "name" => "Sub-activity", + "state" => sub_activity, + "type" => "sensor", + "unique_id" => "sub_activity", + "disabled" => false + }, 3); + break; + } + default: + } + } else { + // System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure"); Settings.unsetWebhookId(); Settings.unsetIsBatteryLevelEnabled(); ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String); @@ -189,7 +240,7 @@ class WebhookManager { } } - function registerWebhookSensor(sensor as Lang.Object) { + function registerWebhookSensor(sensor as Lang.Object, step as Lang.Number) { // System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString()); Communications.makeWebRequest( Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(), @@ -202,7 +253,8 @@ class WebhookManager { :headers => { "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON, + :context => step }, method(:onReturnRegisterWebhookSensor) );