Merge branch 'main' into 342-allow-two-user-supplied-http-headers

This commit is contained in:
Philip Abbey
2026-03-03 20:12:21 +00:00
15 changed files with 74 additions and 170 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ Thumbs.db
source/ClientId.mc source/ClientId.mc
# Gemini API key for automated translations # Gemini API key for automated translations
gemini_api_key.txt gemini_api_key.txt
developer_key

View File

@@ -42,7 +42,7 @@
| 2.27 | Trivial bug fix for the glance view to prevent the "Unconfigured" result being erroneously displayed because the settings were not yet pulled from persistent storage. | | 2.27 | Trivial bug fix for the glance view to prevent the "Unconfigured" result being erroneously displayed because the settings were not yet pulled from persistent storage. |
| 2.28 | Added support for Vivoactive 6 device which also required an SDK update to 8.1.0. | | 2.28 | Added support for Vivoactive 6 device which also required an SDK update to 8.1.0. |
| 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. | | 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. |
| 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. | | 2.30 | <img src="images/Venu2_glance2.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. |
| 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. | | 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. |
| 2.32 | Bug fix for a breaking change extracting options caused by the need to rearrange function parameters for an [annoying compiler error](https://github.com/house-of-abbey/GarminHomeAssistant/issues/253). | | 2.32 | Bug fix for a breaking change extracting options caused by the need to rearrange function parameters for an [annoying compiler error](https://github.com/house-of-abbey/GarminHomeAssistant/issues/253). |
| 3.0 | First version with the ability to use [Wi-Fi or LTE](Wi-Fi.md) instead of Bluetooth but with limited functionality, thanks to [@vincentezw](https://github.com/vincentezw). | | 3.0 | First version with the ability to use [Wi-Fi or LTE](Wi-Fi.md) instead of Bluetooth but with limited functionality, thanks to [@vincentezw](https://github.com/vincentezw). |

View File

@@ -34,7 +34,7 @@ As of version 2.0, there are now two installable versions. For older devices bef
| Version | Explanation | | Version | Explanation |
|------------------------|-------------| |------------------------|-------------|
| Application (original) | For newer devices that allow glance views in their applications, e.g. Venu 2, the GarminHomeAssistant application can be started either from a glance or from the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d28-ec5e-438d-9f83-39e9f45b199d) application page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the application. The application can be started two different ways, either from the glance in the carousel, or as an application from the list of applications & activities. With the latter, it is worth marking the application as a favourite.<br/><img src="images/Venu2_app_start.png" width="200" title="Venu 2" style="margin:5px"/><img src="images/Vivoactive3_app_start.jpg" width="200" title="Vivoactive 3" style="margin:5px"/><br/>If you place the application on your list of favourites, and rearrange it to appear near the top, then the item is just one button press away from the watch face. This second picture here shows the application menu on a Vivoactive 3 watch.<br/><img src="images/Venu2_glance_default.png" width="200" title="Venu 2" style="margin:5px"/><br/>On newer watches, you can also start the application from the glance carousel. The glance view here typically displays some trackable status, so ours provides some early indication of availability. Older watches will still allow you to start this application from the list of applications and activities. | | Application (original) | For newer devices that allow glance views in their applications, e.g. Venu 2, the GarminHomeAssistant application can be started either from a glance or from the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d28-ec5e-438d-9f83-39e9f45b199d) application page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the application. The application can be started two different ways, either from the glance in the carousel, or as an application from the list of applications & activities. With the latter, it is worth marking the application as a favourite.<br/><img src="images/Venu2_app_start.png" width="200" title="Venu 2" style="margin:5px"/><img src="images/Vivoactive3_app_start.jpg" width="200" title="Vivoactive 3" style="margin:5px"/><br/>If you place the application on your list of favourites, and rearrange it to appear near the top, then the item is just one button press away from the watch face. This second picture here shows the application menu on a Vivoactive 3 watch.<br/><img src="./images/Venu2_glance_custom.png" width="200" title="Venu 2" style="margin:5px"/><br/>On newer watches, you can also start the application from the glance carousel. The glance view here typically displays some trackable status, so ours provides some early indication of API availability or something custom as shown here. Older watches will still allow you to start this application from the list of applications and activities. |
| Widget | **"Maintenance only mode"** so no new features will be added to this version.<br>For older devices that use widgets, e.g. Venu (1) as opposed to applications with "glances", the GarminHomeAssistant application can instead be started from the widget carousel. This is a separate item in the Connect IQ AppStore and with this installation, the application will no longer appear in the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/) widget page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the widget.<br/><img src="images/Venu_Widget_sim.png" width="200" title="Venu 2" style="margin:5px"/><br/>Typically the widget view implements something similar to the glance view, e.g. status, and exists in a widget carousel to allow you to select an application to launch.<br>**Please note that memory in widgets is more limited than applications. This means a large menu definition can crash the widget without the code catching the error.**<br> This version was born out of the application version and from Ver 2.0 shared the same source code repository until Ver 2.8 when they were [separated](https://github.com/house-of-abbey/GarminHomeAssistantWidget) to allow the application version to take advantage of its increase memory availability. | | Widget | **"Maintenance only mode"** so no new features will be added to this version.<br>For older devices that use widgets, e.g. Venu (1) as opposed to applications with "glances", the GarminHomeAssistant application can instead be started from the widget carousel. This is a separate item in the Connect IQ AppStore and with this installation, the application will no longer appear in the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/) widget page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the widget.<br/><img src="images/Venu_Widget_sim.png" width="200" title="Venu 2" style="margin:5px"/><br/>Typically the widget view implements something similar to the glance view, e.g. status, and exists in a widget carousel to allow you to select an application to launch.<br>**Please note that memory in widgets is more limited than applications. This means a large menu definition can crash the widget without the code catching the error.**<br> This version was born out of the application version and from Ver 2.0 shared the same source code repository until Ver 2.8 when they were [separated](https://github.com/house-of-abbey/GarminHomeAssistantWidget) to allow the application version to take advantage of its increase memory availability. |
### Features ### Features

View File

@@ -117,6 +117,7 @@ pause
#### API: On-line #### API: On-line
There's an online way of testing the API URL too, thanks to [REQBIN](https://reqbin.com/post-online). This has less setup and it can be saved if you log into the web site. Please note two things: There's an online way of testing the API URL too, thanks to [REQBIN](https://reqbin.com/post-online). This has less setup and it can be saved if you log into the web site. Please note two things:
1. The URL entere below must include a trailing '/' unlike the URL entered into the watch settings. 1. The URL entere below must include a trailing '/' unlike the URL entered into the watch settings.
2. The service imposes a limit on the number requests you can submit without a subscription, so click with purpose! NB. Changing browser buys you a few more clicks. 2. The service imposes a limit on the number requests you can submit without a subscription, so click with purpose! NB. Changing browser buys you a few more clicks.
@@ -125,13 +126,10 @@ There's an online way of testing the API URL too, thanks to [REQBIN](https://req
#### SSL Certificate Chain #### SSL Certificate Chain
With thanks to [@ziceva](https://github.com/ziceva) for solving this problem. The symptoms are: With thanks to [@ziceva](https://github.com/ziceva) for solving this problem. The symptoms are:
1. Using an API URL with SSL (HTTPS), the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) running in a browser on the same phone running Garmin Connect works well. 1. Using an API URL with SSL (HTTPS), the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) running in a browser on the same phone running Garmin Connect works well.
2. The exact same configuration is set in the Garmin HomeAssistant application. 2. The exact same configuration is set in the Garmin HomeAssistant application.
3. The Garmin HomeAssistant application reports: 3. The Garmin HomeAssistant application reports neither the API nor the menu are available.
```
API: not available
Menu: not available
```
**Solution: Make sure you use a _full chain_ certificate in your HTTPS proxy as some watches might be unable to validate the site certificate alone.** **Solution: Make sure you use a _full chain_ certificate in your HTTPS proxy as some watches might be unable to validate the site certificate alone.**
@@ -147,7 +145,6 @@ To verify if you have this issue you can use a tool like [SSL Shoppers's SSL Che
![Full Chain Certificate](images/HTTPS_full_chain.png) ![Full Chain Certificate](images/HTTPS_full_chain.png)
### Top Problems ### Top Problems
1. Failure to copy & paste keys and URLs leading to minor and hard to see errors in strings, even with protestations they are the same! (No they weren't...) 1. Failure to copy & paste keys and URLs leading to minor and hard to see errors in strings, even with protestations they are the same! (No they weren't...)
@@ -158,17 +155,11 @@ To verify if you have this issue you can use a tool like [SSL Shoppers's SSL Che
The [editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) provides the following functions: The [editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) provides the following functions:
1. Syntax highlighting 1. Syntax highlighting
2. Schema checking 2. Schema checking
![Schema checking](images/editor/schema_check.png) ![Schema checking](images/editor/schema_check.png)
3. Entity name completion 3. Entity name completion
![Entity name completion](images/editor/entity_name_completion.png) ![Entity name completion](images/editor/entity_name_completion.png)
4. Rendering previews, to aid HA 'template' creation 4. Rendering previews, to aid HA 'template' creation
![Syntax highlightingL](images/editor/template_preview1.png) ![Syntax highlightingL](images/editor/template_preview1.png)
![Syntax highlightingL](images/editor/template_preview2.png) ![Syntax highlightingL](images/editor/template_preview2.png)
@@ -185,6 +176,7 @@ On (1) you will initially be presented with the following message. Please do not
<div style="margin:30px;padding:20px;background-color:lightgrey;border:1px solid black;border-radius: 10px;"> <div style="margin:30px;padding:20px;background-color:lightgrey;border:1px solid black;border-radius: 10px;">
# GarminHomeAssistant Web Editor # GarminHomeAssistant Web Editor
This is the web editor for the GarminHomeAssistant watch app, it offers enhanced schema checking and validation over the original JSON schema by using the HomeAssistant API to create a schema based on your HomeAssistant configuration. This is the web editor for the GarminHomeAssistant watch app, it offers enhanced schema checking and validation over the original JSON schema by using the HomeAssistant API to create a schema based on your HomeAssistant configuration.
This editor makes use of the same credentials as the watch app (these can be pasted in the top bar of this page). However in order for this editor to work, you will need to amend the CORS settings of your HomeAssistant instance. Add this to your configuration.yaml file: This editor makes use of the same credentials as the watch app (these can be pasted in the top bar of this page). However in order for this editor to work, you will need to amend the CORS settings of your HomeAssistant instance. Add this to your configuration.yaml file:
@@ -215,7 +207,7 @@ For directions on how to write your menu.json file, please see the README in the
For this you will need to have already got the main application or widget working with a menu in order to prove that the API calls are successful. We have proven this works with both our home brew infrastructure as well as Nabu Casa. Now with a script similar to one of the following two, you should be able to fake the watch API call and verify receipt by HomeAssistant. For this you will need to have already got the main application or widget working with a menu in order to prove that the API calls are successful. We have proven this works with both our home brew infrastructure as well as Nabu Casa. Now with a script similar to one of the following two, you should be able to fake the watch API call and verify receipt by HomeAssistant.
#### Battery: Linux, MacOS, UNIX, Cygwin etc ### Battery: Linux, MacOS, UNIX, Cygwin etc
Assume a file called: `send_battery.bash` Assume a file called: `send_battery.bash`
@@ -248,13 +240,13 @@ curl -s -X POST \
Execute: Execute:
``` ```text
$ ./send_battery.bash 45 1 $ ./send_battery.bash 45 1
``` ```
The output looks like this: The output looks like this:
``` ```text
Battery Level = 45 Battery Level = 45
Battery Charging? = true Battery Charging? = true
@@ -315,13 +307,13 @@ pause
Execute: Execute:
``` ```text
> home_assistant_battery_level.cmd 41 1 > home_assistant_battery_level.cmd 41 1
``` ```
The output looks like this: The output looks like this:
``` ```text
"Battery Level = 41" "Battery Level = 41"
"Battery Charging? = true" "Battery Charging? = true"
@@ -345,7 +337,7 @@ There's an online way of testing the API URL too, thanks to [REQBIN](https://req
URL for copy & paste: URL for copy & paste:
``` ```text
https://<Your Domain>/api/webhook/<Your Webhook ID> https://<Your Domain>/api/webhook/<Your Webhook ID>
``` ```
@@ -399,7 +391,7 @@ As a desperate measure to assist with debugging the HomeAssistant Application, y
The figure above shows how to find the file on Windows by attaching your watch by USB cable. Inside the `CIQ_LOG.YML` file there are often multiple entries, each looking like this: The figure above shows how to find the file on Windows by attaching your watch by USB cable. Inside the `CIQ_LOG.YML` file there are often multiple entries, each looking like this:
``` ```text
Error: Unexpected Type Error Error: Unexpected Type Error
Details: 'Failed invoking <symbol>' Details: 'Failed invoking <symbol>'
Time: 2024-08-30T12:00:25Z Time: 2024-08-30T12:00:25Z

View File

@@ -2,15 +2,15 @@
# Glance # Glance
Since [version 2.30](../History.md), it is possible to ovverride the text displayed on the Glance view. This page explains how to customise the text. Since [version 2.30](../History.md), it is possible to override the text displayed on the Glance view. This page explains how to customise the text.
## Default View ## Status View
The default view has always been to display the status of the menu and API availability to indicate if there's a problem. This view has now been updated to be more colourful. The status view displays the accessibility of HomeAssistant API to indicate if there's a problem.
<img src="../images/Venu2_glance_default.png" width="200" title="Venu 2 Default Glance"/> <img src="../images/Venu2_glance_default.png" width="200" title="Venu 2 Default Glance"/>
When either the API or the menu file is inaccessible, the fields will turn red. When API is inaccessible the field will turn red.
## Customised View ## Customised View
@@ -43,9 +43,9 @@ For example:
} }
``` ```
You may make this as complicated as you like! But you have limited space and only ASCII text characters. **It is best to turn on menu caching in order to speed up the display of the template**. The display is then nearly instantaneous. The 'status' view will persist showing until the API becomes available as without the API the custom template cannot be evaluated.
The default view will persist showing until the errors are resolved. In order to extract the custom glance template both the menu and the API are required. So it is logical that the two tests must pass first. The exception here is if the menu is cached, in which case only the API needs to pass. You may make this as complicated as you like! But you have limited space and only ASCII text characters. **It is essential to turn on menu caching in order to display of the template**. This is a change in v3.11 where multiple users are now making larger JSON menus than was originally envisaged for this watch application. As a result the Glance view would fail with an untrapable (fatal) _"Error: Out Of Memory Error"_. A work around is to pull out the glance subsection of the menu and cache that separately during execution of the full application, but that means any changes to the customised Glance view do not show until after the full application has been run.
> [!IMPORTANT] > [!IMPORTANT]
> Sadly what you cannot do is use special characters like: 🌞🔋⛅🪫. Whilst these do display in menu items, they do not seem to work on the Glance view. We really like them, so have tried but failed. Only ASCII text appears to be supported by the Garmin Connect IQ SDK's Glance View. This is not something we have any control over, please do not request this to be "fixed". > Sadly what you cannot do is use special characters like: 🌞🔋⛅🪫. Whilst these do display in menu items, they do not seem to work on the Glance view. We really like them, so have tried but failed. Only ASCII text appears to be supported by the Garmin Connect IQ SDK's Glance View. This is not something we have any control over, please do not request this to be "fixed".
@@ -68,10 +68,10 @@ So the glance view object has a `type` field with two possible values: `info` an
## Displayed Errors ## Displayed Errors
The following shows the default glance when the menu file is not available at the specified URL. The following shows the status Glance view when the API not available at the specified URL.
<img src="../images/Venu2_glance_no_menu.png" width="200" title="Venu 2 Glance showing errors"/> <img src="../images/Venu2_glance_no_api.png" width="200" title="Venu 2 Glance showing errors"/>
Once the custom glance template has been retrieved and evaluated the display will change. Should the connectivity to your HomeAssistant then be lost, e.g. you move out of range of your phone, the glance reflects this in the colour of the residual two rectangles. The top one remains an indicator for the API, and the bottom rectangle remains an indicator for the menu availability, reflecting the original placement in the default glance view that has now been replaced. It is possible to loose connectivity with your HomeAssistant API after connecting and evaluating the Glance template. When the API connection is re-established, the Glance view will update.
<img src="../images/Venu2_glance_no_bt.png" width="200" title="Venu 2 Glance showing lost connectivity"/> <img src="../images/Venu2_glance_no_bt.png" width="200" title="Venu 2 Glance showing lost connectivity"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/Venu2_glance2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -27,11 +27,11 @@ using Toybox.Timer;
// //
(:glance, :background) (:glance, :background)
class HomeAssistantApp extends Application.AppBase { class HomeAssistantApp extends Application.AppBase {
static const scStorageKeyMenu as Lang.String = "menu"; static const scStorageKeyMenu as Lang.String = "menu";
static const scStorageKeyGlance as Lang.String = "glance";
private var mHasToast as Lang.Boolean = false; private var mHasToast as Lang.Boolean = false;
private var mApiStatus as Lang.String?; private var mApiStatus as Lang.String?;
private var mMenuStatus as Lang.String?;
private var mHaMenu as HomeAssistantView?; private var mHaMenu as HomeAssistantView?;
private var mGlanceTemplate as Lang.String? = null; private var mGlanceTemplate as Lang.String? = null;
private var mGlanceText as Lang.String? = null; private var mGlanceText as Lang.String? = null;
@@ -111,7 +111,6 @@ class HomeAssistantApp extends Application.AppBase {
mQuitTimer = new QuitTimer(); mQuitTimer = new QuitTimer();
mUpdateTimer = new Timer.Timer(); mUpdateTimer = new Timer.Timer();
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String; mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
mHasToast = WatchUi has :showToast; mHasToast = WatchUi has :showToast;
Settings.update(); Settings.update();
@@ -166,7 +165,6 @@ class HomeAssistantApp extends Application.AppBase {
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode); // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data); // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data);
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) { switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE: case Communications.BLE_CONNECTION_UNAVAILABLE:
@@ -205,12 +203,8 @@ class HomeAssistantApp extends Application.AppBase {
break; break;
case 200: case 200:
if (data == null) { if (data != null) {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String; if (mIsApp) {
} else {
if (hasCachedMenu()) {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
} else if (mIsApp) {
// var stats = System.getSystemStats(); // stats.* values in bytes // var stats = System.getSystemStats(); // stats.* values in bytes
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Memory: total=" + stats.totalMemory + ", used=" + stats.usedMemory + ", free=" + stats.freeMemory); // System.println("HomeAssistantApp onReturnFetchMenuConfig() Memory: total=" + stats.totalMemory + ", used=" + stats.usedMemory + ", free=" + stats.freeMemory);
@@ -219,14 +213,14 @@ class HomeAssistantApp extends Application.AppBase {
// "Keys and values are limited to 8 KB each, and a total of 128 KB of storage is available." // "Keys and values are limited to 8 KB each, and a total of 128 KB of storage is available."
// "Storage.setValue() fails with an uncatchable out-of-memory error." // "Storage.setValue() fails with an uncatchable out-of-memory error."
Storage.setValue(scStorageKeyMenu, data as Lang.Dictionary); Storage.setValue(scStorageKeyMenu, data as Lang.Dictionary);
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String; // Store the smaller glance section of the menu separately so the Glance view can retrieve it within memory limits.
} else { var glance = (data as Lang.Dictionary)["glance"];
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String; if (glance != null) {
Storage.setValue(scStorageKeyGlance, glance as Lang.Dictionary);
}
} }
} }
if (!mIsApp) { if (mIsApp) {
glanceTemplate(data);
} else {
if (data == null) { if (data == null) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
} else if (data.size() == 0) { } else if (data.size() == 0) {
@@ -268,13 +262,13 @@ class HomeAssistantApp extends Application.AppBase {
function fetchMenuConfig() as Lang.Boolean { function fetchMenuConfig() as Lang.Boolean {
// System.println("Menu URL = " + Settings.getConfigUrl()); // System.println("Menu URL = " + Settings.getConfigUrl());
if (Settings.getConfigUrl().equals("")) { if (Settings.getConfigUrl().equals("")) {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} else { } else {
var menu = Storage.getValue(scStorageKeyMenu) as Lang.Dictionary; var menu = Storage.getValue(scStorageKeyMenu) as Lang.Dictionary;
if (menu != null and (Settings.getClearCache() || !Settings.getCacheConfig())) { if (menu != null and (Settings.getClearCache() || !Settings.getCacheConfig())) {
// System.println("HomeAssistantApp fetchMenuConfig(): Clearing cached menu on user request."); // System.println("HomeAssistantApp fetchMenuConfig(): Clearing cached menu on user request.");
Storage.deleteValue(scStorageKeyMenu); Storage.deleteValue(scStorageKeyMenu);
Storage.deleteValue(scStorageKeyGlance);
menu = null; menu = null;
Settings.unsetClearCache(); Settings.unsetClearCache();
} }
@@ -282,13 +276,8 @@ class HomeAssistantApp extends Application.AppBase {
// System.println("HomeAssistantApp fetchMenuConfig(): Menu not cached, fetching."); // System.println("HomeAssistantApp fetchMenuConfig(): Menu not cached, fetching.");
fetchMenuConfigBasic(method(:onReturnFetchMenuConfig)); fetchMenuConfigBasic(method(:onReturnFetchMenuConfig));
} else { } else {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
WatchUi.requestUpdate(); WatchUi.requestUpdate();
if (!mIsApp) { buildMenu(menu);
glanceTemplate(menu);
} else {
buildMenu(menu);
}
return true; return true;
} }
} }
@@ -329,7 +318,6 @@ class HomeAssistantApp extends Application.AppBase {
} else { } else {
ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String); ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
} }
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
} else { } else {
Communications.makeWebRequest( Communications.makeWebRequest(
Settings.getConfigUrl(), Settings.getConfigUrl(),
@@ -368,19 +356,17 @@ class HomeAssistantApp extends Application.AppBase {
//! Extract the optional template to override the default glance view. //! Extract the optional template to override the default glance view.
// //
function glanceTemplate(menu as Lang.Dictionary) { function glanceTemplate() {
if (menu != null) { var glance = Storage.getValue(scStorageKeyGlance) as Lang.Dictionary;
if (menu["glance"] != null) { if ((glance != null) && (glance["type"] != null)) {
var glance = menu["glance"] as Lang.Dictionary; if (glance["type"].equals("info")) {
if (glance["type"].equals("info")) { mGlanceTemplate = glance["content"] as Lang.String;
mGlanceTemplate = glance["content"] as Lang.String; // System.println("HomeAssistantApp glanceTemplate() " + mGlanceTemplate);
// System.println("HomeAssistantApp glanceTemplate() " + mGlanceTemplate); } else { // if glance["type"].equals("status")
} else { // if glance["type"].equals("status") mGlanceTemplate = null;
mGlanceTemplate = null;
}
} }
} }
} }
//! Test if two dictionaries are structurally equal. Used to see if the JSON menu has been //! Test if two dictionaries are structurally equal. Used to see if the JSON menu has been
//! amended but yet to be updated in the application cache. //! amended but yet to be updated in the application cache.
@@ -539,6 +525,11 @@ class HomeAssistantApp extends Application.AppBase {
if (menu == null || !structuralEquals(data, menu)) { if (menu == null || !structuralEquals(data, menu)) {
// System.println("HomeAssistantApp onReturnCheckMenuConfig() New menu found."); // System.println("HomeAssistantApp onReturnCheckMenuConfig() New menu found.");
Storage.setValue(scStorageKeyMenu, data as Lang.Dictionary); Storage.setValue(scStorageKeyMenu, data as Lang.Dictionary);
// Store the smaller glance section of the menu separately so the Glance view can retrieve it within memory limits.
var glance = (data as Lang.Dictionary)["glance"];
if (glance != null) {
Storage.setValue(scStorageKeyGlance, glance as Lang.Dictionary);
}
if (menu != null) { if (menu != null) {
// Notify the the user we have just got a newer menu file // Notify the the user we have just got a newer menu file
var toast = WatchUi.loadResource($.Rez.Strings.MenuUpdated) as Lang.String; var toast = WatchUi.loadResource($.Rez.Strings.MenuUpdated) as Lang.String;
@@ -897,51 +888,18 @@ class HomeAssistantApp extends Application.AppBase {
switch (responseCode) { switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE: case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
}
break;
case Communications.BLE_QUEUE_FULL: case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
}
break;
case Communications.NETWORK_REQUEST_TIMED_OUT: case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
}
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE: case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
}
break;
case 404: case 404:
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: 404, page not found. Check Configuration URL setting.");
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ConfigUrlNotFound) as Lang.String);
}
break; break;
case 200: case 200:
if ((data != null) && (data instanceof Lang.Dictionary)) { if ((data != null) && (data instanceof Lang.Dictionary)) {
mGlanceText = data["glanceTemplate"]; mGlanceText = data["glanceTemplate"];
} }
WatchUi.requestUpdate();
break; break;
default:
// System.println("HomeAssistantApp onReturnFetchGlanceContent(): Unhandled HTTP response code = " + responseCode);
if (mIsApp) {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
} }
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} }
@@ -989,14 +947,6 @@ class HomeAssistantApp extends Application.AppBase {
return mApiStatus; return mApiStatus;
} }
//! Return the Menu status result.
//!
//! @return A string describing the Menu status
//
function getMenuStatus() as Lang.String {
return mMenuStatus;
}
//! Return the optional glance text that overrides the default glance content. This //! Return the optional glance text that overrides the default glance content. This
//! is derived from the glance template. //! is derived from the glance template.
//! //!
@@ -1047,11 +997,12 @@ class HomeAssistantApp extends Application.AppBase {
function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null { function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
mIsApp = false; // A bit unnecessary given the default mIsApp = false; // A bit unnecessary given the default
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String; mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
Settings.update(); Settings.update();
glanceTemplate();
updateStatus(); updateStatus();
mGlanceTimer = new Timer.Timer(); mGlanceTimer = new Timer.Timer();
mGlanceTimer.start(method(:updateStatus), Globals.scApiBackoffMs, true); mGlanceTimer.start(method(:updateStatus), Globals.scApiBackoffMs, true);
// Although this is now known immediately, wait before displaying so the status can be seen first.
return [new HomeAssistantGlanceView(self)]; return [new HomeAssistantGlanceView(self)];
} }
@@ -1066,8 +1017,6 @@ class HomeAssistantApp extends Application.AppBase {
//! Update the menu and API statuses. Required for the Glance update timer. //! Update the menu and API statuses. Required for the Glance update timer.
// //
function updateStatus() as Void { function updateStatus() as Void {
mGlanceTimer = null;
fetchMenuConfig();
fetchApiStatus(); fetchApiStatus();
if (!Settings.getWebhookId().equals("") && !Settings.getClearWebhookId()) { if (!Settings.getWebhookId().equals("") && !Settings.getClearWebhookId()) {
fetchGlanceContent(); fetchGlanceContent();

View File

@@ -47,8 +47,6 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
private var mTitle as WatchUi.Text?; private var mTitle as WatchUi.Text?;
private var mApiText as WatchUi.Text?; private var mApiText as WatchUi.Text?;
private var mApiStatus as WatchUi.Text?; private var mApiStatus as WatchUi.Text?;
private var mMenuText as WatchUi.Text?;
private var mMenuStatus as WatchUi.Text?;
private var mGlanceContent as WatchUi.TextArea?; private var mGlanceContent as WatchUi.TextArea?;
private var mAntiAlias as Lang.Boolean = false; private var mAntiAlias as Lang.Boolean = false;
@@ -69,7 +67,7 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
function onLayout(dc as Graphics.Dc) as Void { function onLayout(dc as Graphics.Dc) as Void {
var h = dc.getHeight(); var h = dc.getHeight();
mTextWidth = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String + ":", Graphics.FONT_XTINY); mTextWidth = dc.getTextWidthInPixels("API:", Graphics.FONT_XTINY);
mTitle = new WatchUi.Text({ mTitle = new WatchUi.Text({
:text => WatchUi.loadResource($.Rez.Strings.AppName) as Lang.String, :text => WatchUi.loadResource($.Rez.Strings.AppName) as Lang.String,
@@ -86,7 +84,7 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
:font => Graphics.FONT_XTINY, :font => Graphics.FONT_XTINY,
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, :justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin, :locX => scLeftRectMargin + scRectWidth + scRightRectMargin,
:locY => 3 * h / 6 :locY => 4 * h / 6
}); });
mApiStatus = new WatchUi.Text({ mApiStatus = new WatchUi.Text({
:text => WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String, :text => WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String,
@@ -94,28 +92,11 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
:font => Graphics.FONT_XTINY, :font => Graphics.FONT_XTINY,
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, :justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scMidSep + mTextWidth, :locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scMidSep + mTextWidth,
:locY => 3 * h / 6 :locY => 4 * h / 6
});
mMenuText = new WatchUi.Text({
:text => WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String + ":",
:color => Graphics.COLOR_WHITE,
:font => Graphics.FONT_XTINY,
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin,
:locY => 5 * h / 6
});
mMenuStatus = new WatchUi.Text({
:text => WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String,
:color => Graphics.COLOR_WHITE,
:font => Graphics.FONT_XTINY,
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scMidSep + mTextWidth,
:locY => 5 * h / 6
}); });
mGlanceContent = new WatchUi.TextArea({ mGlanceContent = new WatchUi.TextArea({
:text => "A longer piece of text to wrap.", :text => "",
:color => Graphics.COLOR_WHITE, :color => Graphics.COLOR_WHITE,
:font => Graphics.FONT_XTINY, :font => Graphics.FONT_XTINY,
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, :justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
@@ -134,10 +115,8 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
var h = dc.getHeight(); var h = dc.getHeight();
var w = dc.getWidth() - scLeftRectMargin - scRightGlanceMargin; var w = dc.getWidth() - scLeftRectMargin - scRightGlanceMargin;
var apiStatus = mApp.getApiStatus(); var apiStatus = mApp.getApiStatus();
var menuStatus = mApp.getMenuStatus();
var glanceText = mApp.getGlanceText(); var glanceText = mApp.getGlanceText();
var apiCol; var apiCol;
var menuCol;
// System.println("HomeAssistantGlanceView onUpdate() glanceText=" + glanceText); // System.println("HomeAssistantGlanceView onUpdate() glanceText=" + glanceText);
GlanceView.onUpdate(dc); GlanceView.onUpdate(dc);
@@ -160,33 +139,15 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
apiCol = Graphics.COLOR_RED; apiCol = Graphics.COLOR_RED;
} }
if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Checking))) {
menuCol = Graphics.COLOR_YELLOW;
} else if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Available))) {
menuCol = Graphics.COLOR_GREEN;
} else if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Cached))) {
menuCol = Graphics.COLOR_GREEN;
} else {
menuCol = Graphics.COLOR_RED;
}
if (glanceText == null) { if (glanceText == null) {
// Default Glance View // Status Glance View
mApiText.draw(dc); mApiText.draw(dc);
mApiStatus.setText(apiStatus); mApiStatus.setText(apiStatus);
mApiStatus.setColor(apiCol); mApiStatus.setColor(apiCol);
dc.setColor(apiCol, apiCol); dc.setColor(apiCol, apiCol);
dc.drawRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, w, 2 * h / 6 - (2 * scVertMargin), scRectRadius); dc.drawRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, w, 4 * h / 6 - (2 * scVertMargin), scRectRadius);
dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius); dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 4 * h / 6 - (2 * scVertMargin), scRectRadius);
mApiStatus.draw(dc); mApiStatus.draw(dc);
mMenuText.draw(dc);
mMenuStatus.setText(menuStatus);
mMenuStatus.setColor(menuCol);
dc.setColor(menuCol, menuCol);
dc.drawRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, w, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
dc.fillRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
mMenuStatus.draw(dc);
} else { } else {
// Customised Glance View // Customised Glance View
dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_BLUE); dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_BLUE);
@@ -198,9 +159,7 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
scRectRadius scRectRadius
); );
dc.setColor(apiCol, apiCol); dc.setColor(apiCol, apiCol);
dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius); dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 4 * h / 6 - (2 * scVertMargin), scRectRadius);
dc.setColor(menuCol, menuCol);
dc.fillRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
mGlanceContent.setText(glanceText); mGlanceContent.setText(glanceText);
mGlanceContent.draw(dc); mGlanceContent.draw(dc);
} }

View File

@@ -30,17 +30,20 @@ class HomeAssistantNumericPicker extends WatchUi.Picker {
factory as HomeAssistantNumericFactory, factory as HomeAssistantNumericFactory,
haItem as HomeAssistantNumericMenuItem haItem as HomeAssistantNumericMenuItem
) { ) {
mItem = haItem; mItem = haItem;
var picker = mItem.getPicker(); var picker = mItem.getPicker();
var min = (picker.get("min") as Lang.String).toFloat(); var minStr = picker.get("min");
var step = (picker.get("step") as Lang.String).toFloat(); var stepStr = picker.get("step");
var val = haItem.getValue(); var val = haItem.getValue();
if (min == null) { var min = 0.0;
min = 0.0; var step = 1.0;
if (minStr != null) {
min = (minStr as Lang.String).toFloat();
} }
if (step == null) { if (stepStr != null) {
step = 1.0; step = (stepStr as Lang.String).toFloat();
} }
WatchUi.Picker.initialize({ WatchUi.Picker.initialize({
@@ -95,4 +98,4 @@ class HomeAssistantNumericPickerDelegate extends WatchUi.PickerDelegate {
mPicker.onConfirm(values[0]); mPicker.onConfirm(values[0]);
return true; return true;
} }
} }