mirror of
				https://github.com/house-of-abbey/GarminHomeAssistant.git
				synced 2025-10-31 15:48:13 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1098 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			MonkeyC
		
	
	
	
	
	
			
		
		
	
	
			1098 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			MonkeyC
		
	
	
	
	
	
| //-----------------------------------------------------------------------------------
 | |
| //
 | |
| // Distributed under MIT Licence
 | |
| //   See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
 | |
| //
 | |
| //-----------------------------------------------------------------------------------
 | |
| //
 | |
| // GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
 | |
| // tested on a Venu 2 device. The source code is provided at:
 | |
| //            https://github.com/house-of-abbey/GarminHomeAssistant.
 | |
| //
 | |
| // P A Abbey & J D Abbey & Someone0nEarth & moesterheld & vincentezw, 31 October 2023
 | |
| //
 | |
| //-----------------------------------------------------------------------------------
 | |
| 
 | |
| using Toybox.Application;
 | |
| using Toybox.Communications;
 | |
| using Toybox.Lang;
 | |
| // Required for callback method definition
 | |
| typedef Method as Toybox.Lang.Method;
 | |
| using Toybox.WatchUi;
 | |
| using Toybox.System;
 | |
| using Toybox.Application.Properties;
 | |
| using Toybox.Timer;
 | |
| 
 | |
| //! Application root for GarminHomeAssistant
 | |
| //
 | |
| (:glance, :background)
 | |
| class HomeAssistantApp extends Application.AppBase {
 | |
|     private var mHasToast       as Lang.Boolean = false;
 | |
|     private var mApiStatus      as Lang.String?;
 | |
|     private var mMenuStatus     as Lang.String?;
 | |
|     private var mHaMenu         as HomeAssistantView?;  
 | |
|     private var mGlanceTemplate as Lang.String? = null;
 | |
|     private var mGlanceText     as Lang.String? = null;
 | |
|     private var mQuitTimer      as QuitTimer?;
 | |
|     private var mGlanceTimer    as Timer.Timer?;
 | |
|     private var mUpdateTimer    as Timer.Timer?;
 | |
|     // Array initialised by onReturnFetchMenuConfig()
 | |
|     private var mItemsToUpdate  as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or HomeAssistantNumericMenuItem>?;
 | |
|     private var mIsApp          as Lang.Boolean     = false; // Or Widget
 | |
|     private var mUpdating       as Lang.Boolean     = false; // Don't start a second chain of updates
 | |
|     private var mTemplates      as Lang.Dictionary? = null;  // Cache of compiled templates
 | |
|     private var mNotifiedNoBle  as Lang.Boolean     = false;
 | |
|     private var mIsCacheChecked as Lang.Boolean     = false;
 | |
| 
 | |
|     //! Class Constructor
 | |
|     // 
 | |
|     function initialize() {
 | |
|         AppBase.initialize();
 | |
|         // ATTENTION when adding stuff into this block:
 | |
|         // Because of the >>GlanceView<<, it should contain only
 | |
|         // code, which is used as well for the glance:
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/glances/
 | |
|         //
 | |
|         // Also dealing with resources "Rez" needs attention, too. See
 | |
|         // "Resource Scopes":
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/resources/
 | |
|         //
 | |
|         // Classes which are used for the glance view, needed to be tagged
 | |
|         // with "(:glance)".
 | |
|     }
 | |
| 
 | |
|     //! Called on application start up
 | |
|     //!
 | |
|     //! @param state see `AppBase.onStart()`
 | |
|     //
 | |
|     function onStart(state as Lang.Dictionary?) as Void {
 | |
|         AppBase.onStart(state);
 | |
|         // ATTENTION when adding stuff into this block:
 | |
|         // Because of the >>GlanceView<<, it should contain only
 | |
|         // code, which is used as well for the glance:
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/glances/
 | |
|         //
 | |
|         // Also dealing with resources "Rez" needs attention, too. See
 | |
|         // "Resource Scopes":
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/resources/
 | |
|         //
 | |
|         // Classes which are used for the glance view, needed to be tagged
 | |
|         // with "(:glance)".
 | |
|     }
 | |
| 
 | |
|     //! Called when your application is exiting
 | |
|     //
 | |
|     //!
 | |
|     //! @param state see `AppBase.onStop()`
 | |
|     //
 | |
|     function onStop(state as Lang.Dictionary?) as Void {
 | |
|         AppBase.onStop(state);
 | |
|         // ATTENTION when adding stuff into this block:
 | |
|         // Because of the >>GlanceView<<, it should contain only
 | |
|         // code, which is used as well for the glance:
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/glances/
 | |
|         //
 | |
|         // Also dealing with resources "Rez" needs attention, too. See
 | |
|         // "Resource Scopes":
 | |
|         // - https://developer.garmin.com/connect-iq/core-topics/resources/
 | |
|         //
 | |
|         // Classes which are used for the glance view, needed to be tagged
 | |
|         // with "(:glance)".
 | |
|     }
 | |
| 
 | |
|     //! Returns the initial view of the application.
 | |
|     //!
 | |
|     //! @return The initial view.
 | |
|     //
 | |
|     function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
 | |
|         mIsApp       = true;
 | |
|         mQuitTimer   = new QuitTimer();
 | |
|         mUpdateTimer = new Timer.Timer();
 | |
|         mApiStatus   = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
 | |
|         mMenuStatus  = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
 | |
|         mHasToast    = WatchUi has :showToast;
 | |
|         Settings.update();
 | |
| 
 | |
|         if (Settings.getApiKey().length() == 0) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No API key in the application Settings.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoAPIKey) as Lang.String);
 | |
|         } else if (Settings.getApiUrl().length() == 0) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No API URL in the application Settings.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoApiUrl) as Lang.String);
 | |
|         } else if (Settings.getApiUrl().substring(-1, Settings.getApiUrl().length()).equals("/")) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): API URL must not have a trailing slash '/'.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.TrailingSlashErr) as Lang.String);
 | |
|         } else if (Settings.getConfigUrl().length() == 0) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No configuration URL in the application settings.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoConfigUrl) as Lang.String);
 | |
|         } else if (Settings.getPin() == null) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String);
 | |
|         } else if (! System.getDeviceSettings().phoneConnected and Settings.getWifiLteExecutionEnabled() and ! hasCachedMenu()) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No Phone connection, no cached menu, skipping API call.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhoneNoCache) as Lang.String);
 | |
|         } else if (! System.getDeviceSettings().phoneConnected and ! Settings.getWifiLteExecutionEnabled()) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No Phone connection and Wi-Fi disabled, skipping API call.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
 | |
|         } else if (! System.getDeviceSettings().connectionAvailable and ! Settings.getWifiLteExecutionEnabled()) {
 | |
|             // System.println("HomeAssistantApp getInitialView(): No Internet connection and Wi-Fi disabled, skipping API call.");
 | |
|             return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
 | |
|         } else {
 | |
|             var isCached = fetchMenuConfig();
 | |
|             var ret      = null;
 | |
|             fetchApiStatus();
 | |
|             if (isCached) {
 | |
|                 ret = [mHaMenu, new HomeAssistantViewDelegate(true)];
 | |
|             } else {
 | |
|                 ret = [new WatchUi.View(), new WatchUi.BehaviorDelegate()];
 | |
|             }
 | |
|             // Separated from Settings.update() in order to call after fetchMenuConfig() and not call it on changed settings.
 | |
|             Settings.webhook();
 | |
|             return ret;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Callback function after completing the GET request to fetch the configuration menu.
 | |
|     //!
 | |
|     //! @param responseCode Response code.
 | |
|     //! @param data         Response data.
 | |
|     //
 | |
|     function onReturnFetchMenuConfig(
 | |
|         responseCode as Lang.Number,
 | |
|         data         as Null or Lang.Dictionary or Lang.String
 | |
|     ) as Void {
 | |
|         // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode);
 | |
|         // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data);
 | |
| 
 | |
|         mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|         switch (responseCode) {
 | |
|             case Communications.BLE_HOST_TIMEOUT:
 | |
|             case Communications.BLE_CONNECTION_UNAVAILABLE:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: 404, page not found. Check Configuration URL setting.");
 | |
|                 if (mIsApp) {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.ConfigUrlNotFound) as Lang.String);
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             case 200:
 | |
|                 if (data == null) {
 | |
|                     mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|                 } else {
 | |
|                     if (Settings.getCacheConfig()) {
 | |
|                         Storage.setValue("menu", data as Lang.Dictionary);
 | |
|                         mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
 | |
|                     } else {
 | |
|                         mMenuStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
 | |
|                     }
 | |
|                 }
 | |
|                 if (!mIsApp) {
 | |
|                     glanceTemplate(data);
 | |
|                 } else {
 | |
|                     if (data == null) {
 | |
|                         ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
 | |
|                     } else {
 | |
|                         buildMenu(data);
 | |
|                         WatchUi.switchToView(mHaMenu, new HomeAssistantViewDelegate(false), WatchUi.SLIDE_IMMEDIATE);
 | |
|                     }
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchMenuConfig(): Unhandled HTTP response code = " + responseCode);
 | |
|                 if (mIsApp) {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
 | |
|                 }
 | |
|                 break;
 | |
|         }
 | |
|         WatchUi.requestUpdate();
 | |
|     }
 | |
| 
 | |
|     //! Can we use the cached menu?
 | |
|     //!
 | |
|     //! @return Return true if there's a menu in cache, and if the user has enabled the cache and 
 | |
|     //!         has not requested to have the cache refreshed.
 | |
|     //
 | |
|     function hasCachedMenu() as Lang.Boolean {
 | |
|         if (Settings.getClearCache() || !Settings.getCacheConfig()) {
 | |
|             return false;
 | |
|         }
 | |
|         return (Storage.getValue("menu") as Lang.Dictionary) != null;
 | |
|     }
 | |
| 
 | |
|     //! Fetch the menu configuration over HTTPS, which might be locally cached.
 | |
|     //!
 | |
|     //! @return 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.
 | |
|     //
 | |
|     function fetchMenuConfig() as Lang.Boolean {
 | |
|         // System.println("Menu URL = " + Settings.getConfigUrl());
 | |
|         if (Settings.getConfigUrl().equals("")) {
 | |
|             mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
 | |
|             WatchUi.requestUpdate();
 | |
|         } else {
 | |
|             var menu = Storage.getValue("menu") as Lang.Dictionary;
 | |
|             if (menu != null and (Settings.getClearCache() || !Settings.getCacheConfig())) {
 | |
|                 // System.println("HomeAssistantApp fetchMenuConfig(): Clearing cached menu on user request.");
 | |
|                 Storage.deleteValue("menu");
 | |
|                 menu = null;
 | |
|                 Settings.unsetClearCache();
 | |
|             }
 | |
|             if (menu == null) {
 | |
|                 // System.println("HomeAssistantApp fetchMenuConfig(): Menu not cached, fetching.");
 | |
|                 fetchMenuConfigBasic(method(:onReturnFetchMenuConfig));
 | |
|             } else {
 | |
|                 mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
 | |
|                 WatchUi.requestUpdate();
 | |
|                 if (!mIsApp) {
 | |
|                     glanceTemplate(menu);
 | |
|                 } else {
 | |
|                     buildMenu(menu);
 | |
|                 }
 | |
|                 return true;
 | |
|             }
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     //! The basic API call to fetch the menu configuration, with cache management issues managed by external supporting
 | |
|     //! code. This is factored out separately so that it can be reused to check and validate the cached menu.
 | |
|     //!
 | |
|     //! @param responseCallback The method to call on completion of the GET request.
 | |
|     //
 | |
|     function fetchMenuConfigBasic(
 | |
|         responseCallback as (
 | |
|             Method(
 | |
|                 responseCode as Lang.Number,
 | |
|                 data         as Lang.Dictionary or Lang.String or Null
 | |
|             ) as Void
 | |
|         ) or (
 | |
|             Method(
 | |
|                 responseCode as Lang.Number,
 | |
|                 data         as Lang.Dictionary or Lang.String or Null,
 | |
|                 context      as Lang.Object
 | |
|             ) as Void
 | |
|         )
 | |
|     ) {
 | |
|         // System.println("HomeAssistantApp fetchMenuConfigBasic(): Fetching JSON menu.");
 | |
|         var internetAvailable = System.getDeviceSettings().connectionAvailable;
 | |
|         if (! System.getDeviceSettings().phoneConnected or ! internetAvailable) {
 | |
|             // System.println("HomeAssistantApp fetchMenuConfigBasic(): No Phone connection, skipping API call.");
 | |
|             var errorRez = $.Rez.Strings.NoPhone;
 | |
|             if (Settings.getWifiLteExecutionEnabled()) {
 | |
|                 errorRez = $.Rez.Strings.NoPhoneNoCache;
 | |
|             } else if (! internetAvailable) {
 | |
|                 errorRez = $.Rez.Strings.Unavailable;
 | |
|             }
 | |
|             if (!mIsApp) {
 | |
|                 WatchUi.requestUpdate();
 | |
|             } else {
 | |
|                 ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
 | |
|             }
 | |
|             mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
 | |
|         } else {
 | |
|             Communications.makeWebRequest(
 | |
|                 Settings.getConfigUrl(),
 | |
|                 null,
 | |
|                 {
 | |
|                     :method       => Communications.HTTP_REQUEST_METHOD_GET,
 | |
|                     :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
 | |
|                     :headers      => Settings.augmentHttpHeaders({})
 | |
|                 },
 | |
|                 responseCallback
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Build the menu and store in `mHaMenu`. Then start updates if necessary.
 | |
|     //!
 | |
|     //! @param menu The dictionary derived from the JSON menu fetched by `fetchMenuConfig()`.
 | |
|     //
 | |
|     private function buildMenu(menu as Lang.Dictionary) {
 | |
|         mHaMenu = new HomeAssistantView(menu, null);
 | |
|         mQuitTimer.begin();
 | |
|         if (!Settings.getWebhookId().equals("") && !Settings.getClearWebhookId()) {
 | |
|             startUpdates();
 | |
|         } // If not, this will be done via a chain in Settings.webhook() and mWebhookManager.requestWebhookId() that registers the sensors.
 | |
|     }
 | |
| 
 | |
|     //! Start the periodic menu updates for as long as the application is running.
 | |
|     //
 | |
|     function startUpdates() as Void {
 | |
|         if (mHaMenu != null and !mUpdating) {
 | |
|             // Start the continuous update process that continues for as long as the application is running.
 | |
|             mUpdating = true;
 | |
|             updateMenuItems();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Extract the optional template to override the default glance view.
 | |
|     //
 | |
|     function glanceTemplate(menu as Lang.Dictionary) {
 | |
|         if (menu != null) {
 | |
|             if (menu["glance"] != null) {
 | |
|                 var glance = menu["glance"] as Lang.Dictionary;
 | |
|                 if (glance["type"].equals("info")) {
 | |
|                     mGlanceTemplate = glance["content"] as Lang.String;
 | |
|                     // System.println("HomeAssistantApp glanceTemplate() " + mGlanceTemplate);
 | |
|                 } else { // if glance["type"].equals("status")
 | |
|                     mGlanceTemplate = null;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! 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.
 | |
|     //!
 | |
|     //! @param a First dictionary in the comparison.
 | |
|     //! @param b Second dictionary in the comparison.
 | |
|     //
 | |
|     function structuralEquals(
 | |
|         a as Lang.Dictionary,
 | |
|         b as Lang.Dictionary
 | |
|     ) as Lang.Boolean {
 | |
|         if (a.size() != b.size()) {
 | |
|             return false;
 | |
|         }
 | |
|         var keys = a.keys();
 | |
|         for (var i = 0; i < keys.size(); i++) {
 | |
|             var key = keys[i];
 | |
|             // If the sizes are the same and b contains every item in a,
 | |
|             // then a contains every item in b, i.e. the items are the same.
 | |
|             if (!b.hasKey(key)) {
 | |
|                 return false;
 | |
|             }
 | |
|             var valA = a[key];
 | |
|             var valB = b[key];
 | |
|             if (valA == null && valB == null) {
 | |
|                 // both null, consider true
 | |
|             } else if (valA == null || valB == null) {
 | |
|                 return false;
 | |
|             } else if (valA instanceof Lang.Dictionary and valB instanceof Lang.Dictionary) {
 | |
|                 if (!structuralEquals(valA, valB)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             } else if (valA instanceof Lang.Array and valB instanceof Lang.Array) {
 | |
|                 if (!arrayEquals(valA, valB)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             } else if (!valA.equals(valB)) {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     //! Test if two arrays are structurally equal. Used to see if the JSON menu has been
 | |
|     //! amended but yet to be updated in the application cache.
 | |
|     //!
 | |
|     //! @param a First array in the comparison.
 | |
|     //! @param b Second array in the comparison.
 | |
|     //
 | |
|     function arrayEquals(
 | |
|         a as Lang.Array,
 | |
|         b as Lang.Array
 | |
|     ) as Lang.Boolean {
 | |
|         if (a.size() != b.size()) {
 | |
|             return false;
 | |
|         }
 | |
|         for (var i = 0; i < a.size(); i++) {
 | |
|             var itemA = a[i];
 | |
|             var itemB = b[i];
 | |
|             if (itemA == null && itemB == null) {
 | |
|                 // Both null, consider true
 | |
|             } else if (itemA == null || itemB == null) {
 | |
|                 return false;
 | |
|             } else if (itemA instanceof Lang.Dictionary and itemB instanceof Lang.Dictionary) {
 | |
|                 if (!structuralEquals(itemA, itemB)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             } else if (itemA instanceof Lang.Array and itemB instanceof Lang.Array) {
 | |
|                 if (!arrayEquals(itemA, itemB)) {
 | |
|                     return false;
 | |
|                 }
 | |
|             } else if (!itemA.equals(itemB)) {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     //! Takes the actions required to disable the check for an updated menu and notify the user.
 | |
|     //
 | |
|     function disableMenuCheck() as Void {
 | |
|         // As we're out of memory, pretend we've checked and don't try again.
 | |
|         mIsCacheChecked = true;
 | |
|         // Prevent the menu check happening in future.
 | |
|         Settings.unsetMenuCheck();
 | |
|         // Tell the user
 | |
|         var toast = WatchUi.loadResource($.Rez.Strings.MenuCheckDisabled) as Lang.String;
 | |
|         if (mHasToast) {
 | |
|             WatchUi.showToast(toast, null);
 | |
|         } else {
 | |
|             new Alert({
 | |
|                 :timeout => Globals.scAlertTimeoutMs,
 | |
|                 :font    => Graphics.FONT_MEDIUM,
 | |
|                 :text    => toast,
 | |
|                 :fgcolor => Graphics.COLOR_WHITE,
 | |
|                 :bgcolor => Graphics.COLOR_BLACK
 | |
|             }).pushView(WatchUi.SLIDE_IMMEDIATE);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Callback function for the menu check GET request.
 | |
|     //!
 | |
|     //! @param responseCode Response code.
 | |
|     //! @param data         Response data.
 | |
|     //
 | |
|     function onReturnCheckMenuConfig(
 | |
|         responseCode as Lang.Number,
 | |
|         data         as Null or Lang.Dictionary
 | |
|     ) as Void {
 | |
|         // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: " + responseCode);
 | |
|         // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Data: " + data);
 | |
| 
 | |
|         switch (responseCode) {
 | |
|             case Communications.BLE_HOST_TIMEOUT:
 | |
|             case Communications.BLE_CONNECTION_UNAVAILABLE:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.BLE_QUEUE_FULL:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.NETWORK_REQUEST_TIMED_OUT:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
 | |
|                 disableMenuCheck();
 | |
|                 var myTimer = new Timer.Timer();
 | |
|                 myTimer.start(method(:updateMenuItems), Globals.scApiBackoffMs, false);
 | |
|                 break;
 | |
| 
 | |
|             case 404:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: 404, page not found. Check API URL setting.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case 400:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: 400, bad request. Template error.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case 200:
 | |
|                 if (data != null) {
 | |
|                     // 'menu' will be null if caching has just been enabled, but not yet cached locally.
 | |
|                     var menu = Storage.getValue("menu") as Lang.Dictionary;
 | |
|                     if (menu == null || !structuralEquals(data, menu)) {
 | |
|                         // System.println("HomeAssistantApp onReturnCheckMenuConfig() New menu found.");
 | |
|                         Storage.setValue("menu", data as Lang.Dictionary);
 | |
|                         if (menu != null) {
 | |
|                             // Notify the the user we have just got a newer menu file
 | |
|                             var toast = WatchUi.loadResource($.Rez.Strings.MenuUpdated) as Lang.String;
 | |
|                             if (mHasToast) {
 | |
|                                 WatchUi.showToast(toast, null);
 | |
|                             } else {
 | |
|                                 new Alert({
 | |
|                                     :timeout => Globals.scAlertTimeoutMs,
 | |
|                                     :font    => Graphics.FONT_MEDIUM,
 | |
|                                     :text    => toast,
 | |
|                                     :fgcolor => Graphics.COLOR_WHITE,
 | |
|                                     :bgcolor => Graphics.COLOR_BLACK
 | |
|                                 }).pushView(WatchUi.SLIDE_IMMEDIATE);
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                     // Prevent checking the cache is up to date again
 | |
|                     mIsCacheChecked = true;
 | |
|                     var delay = Settings.getPollDelay();
 | |
|                     if (delay > 0) {
 | |
|                         mUpdateTimer.start(method(:updateMenuItems), delay, false);
 | |
|                     } else {
 | |
|                         updateMenuItems();
 | |
|                     }
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 // System.println("HomeAssistantApp onReturnCheckMenuConfig(): Unhandled HTTP response code = " + responseCode);
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Callback function for each menu update GET request.
 | |
|     //!
 | |
|     //! @param responseCode Response code.
 | |
|     //! @param data         Response data.
 | |
|     //
 | |
|     function onReturnUpdateMenuItems(
 | |
|         responseCode as Lang.Number,
 | |
|         data         as Null or Lang.Dictionary
 | |
|     ) as Void {
 | |
|         // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
 | |
|         // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
 | |
| 
 | |
|         var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|         switch (responseCode) {
 | |
|             case Communications.BLE_HOST_TIMEOUT:
 | |
|             case Communications.BLE_CONNECTION_UNAVAILABLE:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.BLE_QUEUE_FULL:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.NETWORK_REQUEST_TIMED_OUT:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
 | |
|                 var myTimer = new Timer.Timer();
 | |
|                 myTimer.start(method(:updateMenuItems), Globals.scApiBackoffMs, false);
 | |
|                 // Revert status
 | |
|                 status = getApiStatus();
 | |
|                 break;
 | |
| 
 | |
|             case 404:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 404, page not found. Check API URL setting.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case 400:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 400, bad request. Template error.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
 | |
|                 break;
 | |
| 
 | |
|             case 200:
 | |
|                 status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
 | |
|                 // System.println("mItemsToUpdate: " + mItemsToUpdate);
 | |
|                 if (data == null) {
 | |
|                     // Simulation and real device behave differently, hence 2nd NoJson error message for "data == null".
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
 | |
|                 } else {
 | |
|                     if (mItemsToUpdate != null) {
 | |
|                         for (var i = 0; i < mItemsToUpdate.size(); i++) {
 | |
|                             var item  = mItemsToUpdate[i];
 | |
|                             var state = data[i.toString()];
 | |
|                             if (item.getTemplate() != null) {
 | |
|                                 item.updateState(state);
 | |
|                             }
 | |
|                             if (item instanceof HomeAssistantToggleMenuItem) {
 | |
|                                 (item as HomeAssistantToggleMenuItem).updateToggleState(data[i.toString() + "t"]);
 | |
|                             }
 | |
|                         }
 | |
|                         if (Settings.getMenuCheck() && Settings.getCacheConfig() && !mIsCacheChecked) {
 | |
|                             // We are caching the menu configuration, so let's fetch it and check if its been updated.
 | |
|                             var stats   = System.getSystemStats(); // stats.* values in bytes
 | |
|                             // https://developer.garmin.com/connect-iq/core-topics/debugging/, see "Basic Debugging"
 | |
|                             // Create a file on the device called /GARMIN/APPS/LOGS/HOMEASSISTANT.TXT in order to log the values here.
 | |
|                             System.println("Memory: total=" + stats.totalMemory + ", used=" + stats.usedMemory + ", free=" + stats.freeMemory);
 | |
|                             if (stats.usedMemory > (Globals.scLowMem * stats.totalMemory)) {
 | |
|                                 // Assume insufficient memory
 | |
|                                 disableMenuCheck();
 | |
|                             } else {
 | |
|                                 // Assume sufficient memory, but the response code might still turn the automatic check off.
 | |
|                                 fetchMenuConfigBasic(method(:onReturnCheckMenuConfig));
 | |
|                             }
 | |
|                         } else {
 | |
|                             var delay = Settings.getPollDelay();
 | |
|                             if (delay > 0) {
 | |
|                                 mUpdateTimer.start(method(:updateMenuItems), delay, false);
 | |
|                             } else {
 | |
|                                 updateMenuItems();
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 // System.println("HomeAssistantApp onReturnUpdateMenuItems(): Unhandled HTTP response code = " + responseCode);
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
 | |
|         }
 | |
|         setApiStatus(status);
 | |
|     }
 | |
| 
 | |
|     //! Construct the GET request to update all menu items.
 | |
|     //
 | |
|     function updateMenuItems() as Void {
 | |
|         if (mUpdating) {
 | |
|             var phoneConnected      = System.getDeviceSettings().phoneConnected;
 | |
|             var connectionAvailable = System.getDeviceSettings().connectionAvailable;
 | |
|             // In Wi-Fi/LTE execution mode, we should not show an error page but use a toast instead.
 | |
|             if (Settings.getWifiLteExecutionEnabled() &&
 | |
|                 (! phoneConnected || ! connectionAvailable)) {
 | |
|                 // Notify only once per disconnection cycle
 | |
|                 if (!mNotifiedNoBle) {
 | |
|                     var toast = WatchUi.loadResource($.Rez.Strings.NoPhone);
 | |
|                     if (!connectionAvailable) {
 | |
|                         toast = WatchUi.loadResource($.Rez.Strings.NoInternet);
 | |
|                     }
 | |
| 
 | |
|                     if (mHasToast) {
 | |
|                         WatchUi.showToast(toast, null);
 | |
|                     } else {
 | |
|                         new Alert({
 | |
|                             :timeout => Globals.scAlertTimeoutMs,
 | |
|                             :font    => Graphics.FONT_MEDIUM,
 | |
|                             :text    => toast,
 | |
|                             :fgcolor => Graphics.COLOR_WHITE,
 | |
|                             :bgcolor => Graphics.COLOR_BLACK
 | |
|                         }).pushView(WatchUi.SLIDE_IMMEDIATE);
 | |
|                     }
 | |
|                 }
 | |
|                 mNotifiedNoBle = true;
 | |
|                 mUpdating      = false;
 | |
|                 setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
 | |
|                 mUpdateTimer.start(method(:startUpdates), Globals.wifiPollResumeDelayMs, false);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if (! phoneConnected) {
 | |
|                 // System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
 | |
|                 setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
 | |
|             } else if (! connectionAvailable) {
 | |
|                 // System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
 | |
|                 ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
 | |
|                 setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
 | |
|             } else {
 | |
|                 mNotifiedNoBle = false;
 | |
|                 if (mItemsToUpdate == null or mTemplates == null) {
 | |
|                     mItemsToUpdate = mHaMenu.getItemsToUpdate();
 | |
|                     mTemplates = {};
 | |
|                     for (var i = 0; i < mItemsToUpdate.size(); i++) {
 | |
|                         var item     = mItemsToUpdate[i];
 | |
|                         var template = item.getTemplate();
 | |
|                         if (template != null) {
 | |
|                             mTemplates.put(i.toString(), {
 | |
|                                 "template" => template
 | |
|                             });
 | |
|                         }
 | |
|                         if (item instanceof HomeAssistantToggleMenuItem) {
 | |
|                             mTemplates.put(i.toString() + "t", {
 | |
|                                 "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate()
 | |
|                             });
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
 | |
|                 // System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
 | |
|                 Communications.makeWebRequest(
 | |
|                     Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
 | |
|                     {
 | |
|                         "type" => "render_template",
 | |
|                         "data" => mTemplates
 | |
|                     },
 | |
|                     {
 | |
|                         :method       => Communications.HTTP_REQUEST_METHOD_POST,
 | |
|                         :headers      => Settings.augmentHttpHeaders({
 | |
|                             "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
 | |
|                         }),
 | |
|                         :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
 | |
|                     },
 | |
|                     method(:onReturnUpdateMenuItems)
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Callback function after completing the GET request to fetch the API status.
 | |
|     //!
 | |
|     //! @param responseCode Response code.
 | |
|     //! @param data         Response data.
 | |
|     //
 | |
|     function onReturnFetchApiStatus(
 | |
|         responseCode as Lang.Number,
 | |
|         data         as Null or Lang.Dictionary or Lang.String
 | |
|     ) as Void {
 | |
|         // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: " + responseCode);
 | |
|         // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data);
 | |
| 
 | |
|         mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|         switch (responseCode) {
 | |
|             case Communications.BLE_HOST_TIMEOUT:
 | |
|             case Communications.BLE_CONNECTION_UNAVAILABLE:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() 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:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: 404, page not found. Check Configuration URL setting.");
 | |
|                 if (mIsApp) {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.ConfigUrlNotFound) as Lang.String);
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             case 200:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: 200.");
 | |
|                 if ((data != null) && (data instanceof Lang.Dictionary) && data["message"].equals("API running.")) {
 | |
|                     mApiStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
 | |
|                 } else {
 | |
|                     if (mIsApp) {
 | |
|                         ErrorView.show("API " + mApiStatus + ".");
 | |
|                     }
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 // System.println("HomeAssistantApp onReturnFetchApiStatus(): Unhandled HTTP response code = " + responseCode);
 | |
|                 if (mIsApp) {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
 | |
|                 }
 | |
|         }
 | |
|         WatchUi.requestUpdate();
 | |
|     }
 | |
| 
 | |
|     //! Construct the GET request to test the API status, is it accessible?
 | |
|     //
 | |
|     function fetchApiStatus() as Void {
 | |
|         var phoneConnected = System.getDeviceSettings().phoneConnected;
 | |
|         var connectionAvailable = System.getDeviceSettings().connectionAvailable;
 | |
| 
 | |
|         // System.println("API URL = " + Settings.getApiUrl());
 | |
|         if (Settings.getApiUrl().equals("")) {
 | |
|             mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
 | |
|             WatchUi.requestUpdate();
 | |
|         } else {
 | |
|             if (mIsApp && Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
 | |
|                 // System.println("HomeAssistantApp fetchApiStatus(): In-app Wifi mode (No Phone and Internet connection), early return.");
 | |
|                 return;
 | |
|             } else if (! phoneConnected) {
 | |
|                 // System.println("HomeAssistantApp fetchApiStatus(): No Phone connection, skipping API call.");
 | |
|                 mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|                 if (!mIsApp) {
 | |
|                     WatchUi.requestUpdate();
 | |
|                 } else {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
 | |
|                 }
 | |
|             } else if (! connectionAvailable) {
 | |
|                 // System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call.");
 | |
|                 mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
 | |
|                 if (!mIsApp) {
 | |
|                     WatchUi.requestUpdate();
 | |
|                 } else {
 | |
|                     ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
 | |
|                 }
 | |
|             } else {
 | |
|                 Communications.makeWebRequest(
 | |
|                     Settings.getApiUrl() + "/",
 | |
|                     null,
 | |
|                     {
 | |
|                         :method       => Communications.HTTP_REQUEST_METHOD_GET,
 | |
|                         :headers      => Settings.augmentHttpHeaders({
 | |
|                             "Authorization" => "Bearer " + Settings.getApiKey()
 | |
|                         }),
 | |
|                         :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
 | |
|                     },
 | |
|                     method(:onReturnFetchApiStatus)
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     //! Callback function after completing the GET request to render the glance template.
 | |
|     //!
 | |
|     //! @param responseCode Response code.
 | |
|     //! @param data         Response data.
 | |
|     //
 | |
|     function onReturnFetchGlanceContent(
 | |
|         responseCode as Lang.Number,
 | |
|         data         as Null or Lang.Dictionary or Lang.String
 | |
|     ) as Void {
 | |
|         // System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: " + responseCode);
 | |
|         // System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Data: " + data);
 | |
| 
 | |
|         switch (responseCode) {
 | |
|             case Communications.BLE_HOST_TIMEOUT:
 | |
|             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:
 | |
|                 // 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:
 | |
|                 // 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:
 | |
|                 // 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:
 | |
|                 // 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;
 | |
| 
 | |
|             case 200:
 | |
|                 if ((data != null) && (data instanceof Lang.Dictionary)) {
 | |
|                     mGlanceText = data["glanceTemplate"];
 | |
|                 }
 | |
|                 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();
 | |
|     }
 | |
| 
 | |
|     //! Construct the GET request to convert the optional glance template to text for display.
 | |
|     //
 | |
|     function fetchGlanceContent() as Void {
 | |
|         if (mGlanceTemplate != null) {
 | |
|             // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
 | |
|             Communications.makeWebRequest(
 | |
|                 Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
 | |
|                 {
 | |
|                     "type" => "render_template",
 | |
|                     "data" => {
 | |
|                         "glanceTemplate" => {
 | |
|                             "template" => mGlanceTemplate
 | |
|                         }
 | |
|                     }
 | |
|                 },
 | |
|                 {
 | |
|                     :method       => Communications.HTTP_REQUEST_METHOD_POST,
 | |
|                     :headers      => Settings.augmentHttpHeaders({
 | |
|                         "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
 | |
|                     }),
 | |
|                     :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
 | |
|                 },
 | |
|                 method(:onReturnFetchGlanceContent)
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Record the API status result.
 | |
|     //!
 | |
|     //! @param s A string describing the API status
 | |
|     //
 | |
|     function setApiStatus(s as Lang.String) {
 | |
|         mApiStatus = s;
 | |
|     }
 | |
| 
 | |
|     //! Return the API status result.
 | |
|     //!
 | |
|     //! @return A string describing the API status
 | |
|     //
 | |
|     function getApiStatus() as Lang.String {
 | |
|         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
 | |
|     //! is derived from the glance template.
 | |
|     //!
 | |
|     //! @return A string derived from the glance template (or null)
 | |
|     //
 | |
|     function getGlanceText() as Lang.String? {
 | |
|         return mGlanceText;
 | |
|     }
 | |
| 
 | |
|     //! Return the Menu construction status.
 | |
|     //!
 | |
|     //! @return A Boolean indicating if the menu is loaded into the application.
 | |
|     //
 | |
|     function isHomeAssistantMenuLoaded() as Lang.Boolean {
 | |
|         return mHaMenu != null;
 | |
|     }
 | |
| 
 | |
|     //! Make the menu visible on the watch face.
 | |
|     //
 | |
|     function pushHomeAssistantMenuView() as Void {
 | |
|         WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
 | |
|     }
 | |
| 
 | |
|     //! Force status updates. Only take action if `Settings.getPollDelay() > 0`. This must be tested
 | |
|     //! locally as it is then efficient to take alternative action if the test fails.
 | |
|     //
 | |
|     function forceStatusUpdates() as Void {
 | |
|         // Don't mess with updates unless we are using a timer.
 | |
|         if (Settings.getPollDelay() > 0) {
 | |
|             mUpdateTimer.stop();
 | |
|             // For immediate updates
 | |
|             updateMenuItems();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Return the timer used to quit the application.
 | |
|     //!
 | |
|     //! @return Timer object
 | |
|     //
 | |
|     function getQuitTimer() as QuitTimer {
 | |
|         return mQuitTimer;
 | |
|     }
 | |
| 
 | |
|     //! Return the glance view.
 | |
|     //!
 | |
|     //! @return The glance view
 | |
|     //
 | |
|     function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
 | |
|         mIsApp      = false; // A bit unnecessary given the default
 | |
|         mApiStatus  = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
 | |
|         mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
 | |
|         Settings.update();
 | |
|         updateStatus();
 | |
|         mGlanceTimer = new Timer.Timer();
 | |
|         mGlanceTimer.start(method(:updateStatus), Globals.scApiBackoffMs, true);
 | |
|         return [new HomeAssistantGlanceView(self)];
 | |
|     }
 | |
| 
 | |
|     //! Return the glance theme.
 | |
|     //!
 | |
|     //! @return The glance colour
 | |
|     //
 | |
|     function getGlanceTheme() as Application.AppBase.GlanceTheme {
 | |
|         return Application.AppBase.GLANCE_THEME_LIGHT_BLUE;
 | |
|     }
 | |
| 
 | |
|     //! Update the menu and API statuses. Required for the Glance update timer.
 | |
|     //
 | |
|     function updateStatus() as Void {
 | |
|         mGlanceTimer = null;
 | |
|         fetchMenuConfig();
 | |
|         fetchApiStatus();
 | |
|         if (!Settings.getWebhookId().equals("") && !Settings.getClearWebhookId()) {
 | |
|             fetchGlanceContent();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Code for when the application settings are updated.
 | |
|     //
 | |
|     function onSettingsChanged() as Void {
 | |
|         // System.println("HomeAssistantApp onSettingsChanged()");
 | |
|         Settings.update();
 | |
|     }
 | |
| 
 | |
|     //! Called each time the Registered Temporal Event is to be invoked. So the object is created each time
 | |
|     //! on request and then destroyed on completion (to save resources).
 | |
|     //
 | |
|     function getServiceDelegate() as [ System.ServiceDelegate ] {
 | |
|         return [new BackgroundServiceDelegate()];
 | |
|     }
 | |
| 
 | |
|     //! Determine is we are a glance or the full application. Glances should be considered to be separate applications.
 | |
|     //!
 | |
|     //! @return We are an application (if not we're a glance)
 | |
|     //
 | |
|     function getIsApp() as Lang.Boolean {
 | |
|         return mIsApp;
 | |
|     }
 | |
| 
 | |
|     //! Returns a SyncDelegate for this App
 | |
|     //!
 | |
|     //! @return a SyncDelegate or null
 | |
|     //
 | |
|     public function getSyncDelegate() as Communications.SyncDelegate? {
 | |
|         return new HomeAssistantSyncDelegate();
 | |
|     }
 | |
| }
 | |
| 
 | |
| //! Global function to return the application object.
 | |
| //!
 | |
| //! @return The application object.
 | |
| //
 | |
| (:glance, :background)
 | |
| function getApp() as HomeAssistantApp {
 | |
|     return Application.getApp() as HomeAssistantApp;
 | |
| }
 |