Initial solution

This commit is contained in:
Philip Abbey
2024-01-08 00:08:12 +00:00
parent 24ebc72080
commit 0a2d257421
8 changed files with 128 additions and 50 deletions

View File

@ -24,6 +24,12 @@
<!-- Best be a public URL in order to work away from your home LAN and have a trusted HTTPS certificate --> <!-- Best be a public URL in order to work away from your home LAN and have a trusted HTTPS certificate -->
<property id="config_url" type="string"></property> <property id="config_url" type="string"></property>
<!-- Decide if the menu configuration should be cached. -->
<property id="cache_config" type="boolean">false</property>
<!-- Clear the menu configuration on next application start, and refetch, then set this back to false -->
<property id="clear_cache" type="boolean">false</property>
<!-- <!--
Application timeout in seconds, except 0 for no timeout (default). After this amount of elapsed time Application timeout in seconds, except 0 for no timeout (default). After this amount of elapsed time
with no activity, exit the application. with no activity, exit the application.

View File

@ -37,6 +37,20 @@
<settingConfig type="alphaNumeric" /> <settingConfig type="alphaNumeric" />
</setting> </setting>
<setting
propertyKey="@Properties.cache_config"
title="@Strings.SettingsCacheConfig"
>
<settingConfig type="boolean" />
</setting>
<setting
propertyKey="@Properties.clear_cache"
title="@Strings.SettingsClearCache"
>
<settingConfig type="boolean" />
</setting>
<setting <setting
propertyKey="@Properties.app_timeout" propertyKey="@Properties.app_timeout"
title="@Strings.SettingsAppTimeout" title="@Strings.SettingsAppTimeout"

View File

@ -35,6 +35,7 @@
<string id="Checking" scope="glance">Checking...</string> <string id="Checking" scope="glance">Checking...</string>
<string id="Unavailable" scope="glance">Unavailable</string> <string id="Unavailable" scope="glance">Unavailable</string>
<string id="Unconfigured" scope="glance">Unconfigured</string> <string id="Unconfigured" scope="glance">Unconfigured</string>
<string id="Cached" scope="glance">Cached</string>
<string id="GlanceMenu" scope="glance">Menu</string> <string id="GlanceMenu" scope="glance">Menu</string>
<!-- For the settings GUI --> <!-- For the settings GUI -->
@ -43,6 +44,8 @@
<string id="SettingsApiKeyPrompt">Long-Lived Access Token.</string> <string id="SettingsApiKeyPrompt">Long-Lived Access Token.</string>
<string id="SettingsApiUrl">URL for HomeAssistant API.</string> <string id="SettingsApiUrl">URL for HomeAssistant API.</string>
<string id="SettingsConfigUrl">URL for menu configuration (JSON).</string> <string id="SettingsConfigUrl">URL for menu configuration (JSON).</string>
<string id="SettingsCacheConfig">Should the application cache the menu configuration?</string>
<string id="SettingsClearCache">Should the application clear the existing cache next time it is started?</string>
<string id="SettingsAppTimeout">Timeout in seconds. Exit the application after this period of inactivity to save the device battery.</string> <string id="SettingsAppTimeout">Timeout in seconds. Exit the application after this period of inactivity to save the device battery.</string>
<string id="SettingsConfirmTimeout">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.</string> <string id="SettingsConfirmTimeout">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.</string>
<string id="SettingsMenuItemStyle">Menu item style.</string> <string id="SettingsMenuItemStyle">Menu item style.</string>

View File

@ -124,12 +124,16 @@ class HomeAssistantApp extends Application.AppBase {
} }
return ErrorView.create(RezStrings.getNoInternet() + "."); return ErrorView.create(RezStrings.getNoInternet() + ".");
} else { } else {
fetchMenuConfig(); var isCached = fetchMenuConfig();
fetchApiStatus(); fetchApiStatus();
if (WidgetApp.isWidget) { if (WidgetApp.isWidget) {
return [new RootView(self), new RootViewDelegate(self)] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>; return [new RootView(self), new RootViewDelegate(self)] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>;
} else { } else {
return [new WatchUi.View(), new WatchUi.BehaviorDelegate()] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>; if (isCached) {
return [mHaMenu, new HomeAssistantViewDelegate(true)] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>;
} else {
return [new WatchUi.View(), new WatchUi.BehaviorDelegate()] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>;
}
} }
} }
} }
@ -193,21 +197,11 @@ class HomeAssistantApp extends Application.AppBase {
case 200: case 200:
mMenuStatus = RezStrings.getAvailable(); mMenuStatus = RezStrings.getAvailable();
if (Settings.getCacheConfig()) {
Storage.setValue("menu", data as Lang.Dictionary);
}
if (!mIsGlance) { if (!mIsGlance) {
mHaMenu = new HomeAssistantView(data, null); buildMenu(data);
mQuitTimer.begin();
if (Settings.getIsWidgetStartNoTap()) {
// As soon as the menu has been fetched start show the menu of items.
// This behaviour is inconsistent with the standard Garmin User Interface, but has been
// requested by users so has been made the non-default option.
pushHomeAssistantMenuView();
}
mItemsToUpdate = mHaMenu.getItemsToUpdate();
// Start the continuous update process that continues for as long as the application is running.
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
if (mItemsToUpdate.size() > 0) {
updateNextMenuItem();
}
if (!WidgetApp.isWidget) { if (!WidgetApp.isWidget) {
WatchUi.switchToView(mHaMenu, new HomeAssistantViewDelegate(false), WatchUi.SLIDE_IMMEDIATE); WatchUi.switchToView(mHaMenu, new HomeAssistantViewDelegate(false), WatchUi.SLIDE_IMMEDIATE);
} }
@ -226,43 +220,77 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} }
// Return true if the menu came from the cache, otherwise false. This is because fetching the menu when not in the cache is
// asynchronous and affects how the views are managed.
(:glance) (:glance)
function fetchMenuConfig() as Void { function fetchMenuConfig() as Lang.Boolean {
if (Settings.getConfigUrl().equals("")) { if (Settings.getConfigUrl().equals("")) {
mMenuStatus = RezStrings.getUnconfigured(); mMenuStatus = RezStrings.getUnconfigured();
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} else { } else {
if (! System.getDeviceSettings().phoneConnected) { var menu = Storage.getValue("menu") as Lang.Dictionary;
if (Globals.scDebug) { if (menu != null and Settings.getClearCache()) {
System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call."); Storage.deleteValue("menu");
} menu = null;
if (mIsGlance) { Settings.unsetClearCache();
WatchUi.requestUpdate();
} else {
ErrorView.show(RezStrings.getNoPhone() + ".");
}
mMenuStatus = RezStrings.getUnavailable();
} else if (! System.getDeviceSettings().connectionAvailable) {
if (Globals.scDebug) {
System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
}
if (mIsGlance) {
WatchUi.requestUpdate();
} else {
ErrorView.show(RezStrings.getNoInternet() + ".");
}
mMenuStatus = RezStrings.getUnavailable();
} else {
Communications.makeWebRequest(
Settings.getConfigUrl(),
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnFetchMenuConfig)
);
} }
if (menu == null) {
if (! System.getDeviceSettings().phoneConnected) {
if (Globals.scDebug) {
System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
}
if (mIsGlance) {
WatchUi.requestUpdate();
} else {
ErrorView.show(RezStrings.getNoPhone() + ".");
}
mMenuStatus = RezStrings.getUnavailable();
} else if (! System.getDeviceSettings().connectionAvailable) {
if (Globals.scDebug) {
System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
}
if (mIsGlance) {
WatchUi.requestUpdate();
} else {
ErrorView.show(RezStrings.getNoInternet() + ".");
}
mMenuStatus = RezStrings.getUnavailable();
} else {
Communications.makeWebRequest(
Settings.getConfigUrl(),
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnFetchMenuConfig)
);
}
} else {
mMenuStatus = RezStrings.getCached();
if (!mIsGlance) {
buildMenu(menu);
}
return true;
}
}
return false;
}
private function buildMenu(menu as Lang.Dictionary) {
mHaMenu = new HomeAssistantView(menu, null);
mQuitTimer.begin();
if (Settings.getIsWidgetStartNoTap()) {
// As soon as the menu has been fetched start show the menu of items.
// This behaviour is inconsistent with the standard Garmin User Interface, but has been
// requested by users so has been made the non-default option.
pushHomeAssistantMenuView();
}
mItemsToUpdate = mHaMenu.getItemsToUpdate();
// Start the continuous update process that continues for as long as the application is running.
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
if (mItemsToUpdate.size() > 0) {
updateNextMenuItem();
} }
} }

View File

@ -102,6 +102,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
var myTimer = new Timer.Timer(); var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false); myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break; break;
case 404: case 404:

View File

@ -26,8 +26,8 @@ using Toybox.WatchUi;
class HomeAssistantView extends WatchUi.Menu2 { class HomeAssistantView extends WatchUi.Menu2 {
// List of items that need to have their status updated periodically // List of items that need to have their status updated periodically
private var mListToggleItems = []; private var mListToggleItems = [];
private var mListMenuItems = []; private var mListMenuItems = [];
function initialize( function initialize(
definition as Lang.Dictionary, definition as Lang.Dictionary,

View File

@ -55,6 +55,8 @@ class RezStrings {
(:glance) (:glance)
private static var strUnconfigured as Lang.String or Null; private static var strUnconfigured as Lang.String or Null;
(:glance) (:glance)
private static var strCached as Lang.String or Null;
(:glance)
private static var strGlanceMenu as Lang.String or Null; private static var strGlanceMenu as Lang.String or Null;
private static var strLabelToggle as Lang.Dictionary or Null; private static var strLabelToggle as Lang.Dictionary or Null;
@ -71,6 +73,7 @@ class RezStrings {
strChecking = WatchUi.loadResource($.Rez.Strings.Checking); strChecking = WatchUi.loadResource($.Rez.Strings.Checking);
strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable); strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable);
strUnconfigured = WatchUi.loadResource($.Rez.Strings.Unconfigured); strUnconfigured = WatchUi.loadResource($.Rez.Strings.Unconfigured);
strCached = WatchUi.loadResource($.Rez.Strings.Cached);
strGlanceMenu = WatchUi.loadResource($.Rez.Strings.GlanceMenu); strGlanceMenu = WatchUi.loadResource($.Rez.Strings.GlanceMenu);
} }
@ -97,6 +100,7 @@ class RezStrings {
strChecking = WatchUi.loadResource($.Rez.Strings.Checking); strChecking = WatchUi.loadResource($.Rez.Strings.Checking);
strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable); strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable);
strUnconfigured = WatchUi.loadResource($.Rez.Strings.Unconfigured); strUnconfigured = WatchUi.loadResource($.Rez.Strings.Unconfigured);
strCached = WatchUi.loadResource($.Rez.Strings.Cached);
strGlanceMenu = WatchUi.loadResource($.Rez.Strings.GlanceMenu); strGlanceMenu = WatchUi.loadResource($.Rez.Strings.GlanceMenu);
strLabelToggle = { strLabelToggle = {
:enabled => WatchUi.loadResource($.Rez.Strings.MenuItemOn) as Lang.String, :enabled => WatchUi.loadResource($.Rez.Strings.MenuItemOn) as Lang.String,
@ -184,6 +188,10 @@ class RezStrings {
return strUnconfigured; return strUnconfigured;
} }
static function getCached() as Lang.String {
return strCached;
}
static function getGlanceMenu() as Lang.String { static function getGlanceMenu() as Lang.String {
return strGlanceMenu; return strGlanceMenu;
} }

View File

@ -34,6 +34,8 @@ class Settings {
private static var mApiKey as Lang.String = ""; private static var mApiKey as Lang.String = "";
private static var mApiUrl as Lang.String = ""; private static var mApiUrl as Lang.String = "";
private static var mConfigUrl as Lang.String = ""; private static var mConfigUrl as Lang.String = "";
private static var mCacheConfig as Lang.Boolean = false;
private static var mClearCache as Lang.Boolean = false;
private static var mAppTimeout as Lang.Number = 0; // seconds private static var mAppTimeout as Lang.Number = 0; // seconds
private static var mConfirmTimeout as Lang.Number = 3; // seconds private static var mConfirmTimeout as Lang.Number = 3; // seconds
private static var mMenuStyle as Lang.Number = MENU_STYLE_ICONS; private static var mMenuStyle as Lang.Number = MENU_STYLE_ICONS;
@ -49,6 +51,8 @@ class Settings {
mApiKey = Properties.getValue("api_key"); mApiKey = Properties.getValue("api_key");
mApiUrl = Properties.getValue("api_url"); mApiUrl = Properties.getValue("api_url");
mConfigUrl = Properties.getValue("config_url"); mConfigUrl = Properties.getValue("config_url");
mCacheConfig = Properties.getValue("cache_config");
mClearCache = Properties.getValue("clear_cache");
mAppTimeout = Properties.getValue("app_timeout"); mAppTimeout = Properties.getValue("app_timeout");
mConfirmTimeout = Properties.getValue("confirm_timeout"); mConfirmTimeout = Properties.getValue("confirm_timeout");
mMenuStyle = Properties.getValue("menu_theme"); mMenuStyle = Properties.getValue("menu_theme");
@ -100,6 +104,19 @@ class Settings {
return mConfigUrl; return mConfigUrl;
} }
static function getCacheConfig() as Lang.Boolean {
return mCacheConfig;
}
static function getClearCache() as Lang.Boolean {
return mClearCache;
}
static function unsetClearCache() {
mClearCache = false;
Properties.setValue("clear_cache", mClearCache);
}
static function getAppTimeout() as Lang.Number { static function getAppTimeout() as Lang.Number {
return mAppTimeout * 1000; // Convert to milliseconds return mAppTimeout * 1000; // Convert to milliseconds
} }