From 172d4ad1e4bd7eb8a316cb3318d99ae467e4a147 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Thu, 10 Jul 2025 16:50:15 +0100 Subject: [PATCH 01/11] add Wifi LTE command execution --- resources/settings/properties.xml | 5 + resources/settings/settings.xml | 7 + resources/strings/strings.xml | 7 + source/HomeAssistantApp.mc | 55 ++++-- source/HomeAssistantService.mc | 19 ++- source/HomeAssistantToggleMenuItem.mc | 60 +++++-- source/Settings.mc | 10 ++ source/WifiLteExecution.mc | 236 ++++++++++++++++++++++++++ 8 files changed, 368 insertions(+), 31 deletions(-) create mode 100644 source/WifiLteExecution.mc diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml index 4dc885c..55bdd79 100644 --- a/resources/settings/properties.xml +++ b/resources/settings/properties.xml @@ -96,4 +96,9 @@ --> + + false diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml index 333b5ae..17552bb 100644 --- a/resources/settings/settings.xml +++ b/resources/settings/settings.xml @@ -116,4 +116,11 @@ > + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index 9ff1d3c..ec32446 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -31,7 +31,9 @@ No Internet connection. No JSON returned from HTTP request. No Phone connection. + No phone connection, no cached menu. No Response, check Internet connection + Request timed out PIN input locked for Potential Error seconds @@ -42,6 +44,10 @@ HTTP request returned error code = Failed to register Webhook Wrong PIN + No Wifi or LTE available + Execute over Wifi/LTE? + Sending to Home Assistant. + No data received. Select... @@ -64,4 +70,5 @@ Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant. The refresh rate (in minutes) at which the background service should repeat sending data. (Read only) The Webhook ID created by the device for background service updates. You might require this for debugging. + Allows the app to start without phone connection, and prompt to execute command over Wifi/LTE. diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index bda9906..5f0324e 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -14,6 +14,7 @@ //----------------------------------------------------------------------------------- using Toybox.Application; +using Toybox.Communications; using Toybox.Lang; using Toybox.WatchUi; using Toybox.System; @@ -122,11 +123,14 @@ class HomeAssistantApp extends Application.AppBase { } 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) { - // System.println("HomeAssistantApp getInitialView(): No Phone connection, skipping API call."); + } 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 wifi disabled, skipping API call."); return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { - // System.println("HomeAssistantApp getInitialView(): No Internet connection, skipping API call."); + } else if (! System.getDeviceSettings().connectionAvailable and ! Settings.getWifiLteExecutionEnabled()) { + // System.println("HomeAssistantApp getInitialView(): No Internet connection and wifi disabled, skipping API call."); return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); } else { var isCached = fetchMenuConfig(); @@ -227,6 +231,20 @@ class HomeAssistantApp extends Application.AppBase { 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 busted. + // + function hasCachedMenu() as Lang.Boolean { + if (Settings.getClearCache() || !Settings.getCacheConfig()) { + return false; + } + + var menu = Storage.getValue("menu") as Lang.Dictionary; + return menu != 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 @@ -246,22 +264,22 @@ class HomeAssistantApp extends Application.AppBase { Settings.unsetClearCache(); } if (menu == null) { - if (! System.getDeviceSettings().phoneConnected) { + var phoneConnected = System.getDeviceSettings().phoneConnected; + var internetAvailable = System.getDeviceSettings().connectionAvailable; + if (! phoneConnected or ! internetAvailable) { + var errorRez = $.Rez.Strings.NoPhone; + if (Settings.getWifiLteExecutionEnabled()) { + errorRez = $.Rez.Strings.NoPhoneNoCache; + } else if (! internetAvailable) { + errorRez = $.Rez.Strings.Unavailable; + } // System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call."); if (mIsGlance) { WatchUi.requestUpdate(); } else { - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); + ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String); } - mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String; - } else if (! System.getDeviceSettings().connectionAvailable) { - // System.println("HomeAssistantApp fetchMenuConfig(): No Internet connection, skipping API call."); - if (mIsGlance) { - WatchUi.requestUpdate(); - } else { - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); - } - mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String; + mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String; } else { Communications.makeWebRequest( Settings.getConfigUrl(), @@ -785,6 +803,13 @@ class HomeAssistantApp extends Application.AppBase { 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. diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc index 104f800..eca2509 100644 --- a/source/HomeAssistantService.mc +++ b/source/HomeAssistantService.mc @@ -129,10 +129,25 @@ class HomeAssistantService { data as Lang.Dictionary or Null, exit as Lang.Boolean ) as Void { - if (! System.getDeviceSettings().phoneConnected) { + var phoneConnected = System.getDeviceSettings().phoneConnected; + var internetAvailable = System.getDeviceSettings().connectionAvailable; + if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! internetAvailable)) { + var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; + var dialog = new WatchUi.Confirmation(dialogMsg); + WatchUi.pushView( + dialog, + new WifiLteExecutionConfirmDelegate({ + :type => "service", + :service => service, + :data => data, + :exit => exit, + }, null), + WatchUi.SLIDE_LEFT + ); + } else if (! phoneConnected) { // System.println("HomeAssistantService call(): No Phone connection, skipping API call."); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { + } else if (! internetAvailable) { // System.println("HomeAssistantService call(): No Internet connection, skipping API call."); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); } else { diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index e902916..e3bf4a9 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -198,16 +198,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { case 200: // System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed."); getApp().forceStatusUpdates(); - var state; var d = data as Lang.Array; - for(var i = 0; i < d.size(); i++) { - if ((d[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) { - state = d[i].get("state") as Lang.String; - // System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); - setUiToggle(state); - WatchUi.requestUpdate(); - } - } + setToggleStateWithData(d); status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String; break; @@ -221,23 +213,41 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } } + //! Handles the response from a Home Assistant service or state call and updates the toggle UI. + //! + //! @param data An array of dictionaries, each representing a Home Assistant entity state. + // + function setToggleStateWithData(data as Lang.Array) { + for(var i = 0; i < data.size(); i++) { + if ((data[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) { + var state = data[i].get("state") as Lang.String; + // System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); + setUiToggle(state); + WatchUi.requestUpdate(); + } + } + } + //! Set the state of the toggle menu item. //! //! @param s Boolean indicating the desired state of the toggle switch. // function setState(s as Lang.Boolean) as Void { // Toggle the UI back, we'll wait for confirmation from the Home Assistant + // Note: with Zigbee2MQTT a.o. we may not always get the state in the response. setEnabled(!isEnabled()); - if (! System.getDeviceSettings().phoneConnected) { + + var phoneConnected = System.getDeviceSettings().phoneConnected; + var internetAvailable = System.getDeviceSettings().connectionAvailable; + + if (! phoneConnected && ! Settings.getWifiLteExecutionEnabled()) { // System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call."); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { + } else if (! internetAvailable && ! Settings.getWifiLteExecutionEnabled()) { // System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call."); // Toggle the UI back ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); } else { - // Updated SDK and got a new error - // ERROR: venu: Cannot find symbol ':substring' on type 'PolyType'. var id = mData.get("entity_id") as Lang.String; var url = Settings.getApiUrl() + "/services/"; if (s) { @@ -245,6 +255,28 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } else { url = url + id.substring(0, id.find(".")) + "/turn_off"; } + + if (! phoneConnected && ! internetAvailable && Settings.getWifiLteExecutionEnabled()) { + var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; + var dialog = new WatchUi.Confirmation(dialogMsg); + WatchUi.pushView( + dialog, + new WifiLteExecutionConfirmDelegate({ + :type => "entity", + :url => url, + :id => id, + :data => mData, + :callback => method(:setToggleStateWithData), + :exit => mExit, + }, { + :confirmMethod => method(:onConfirm), + :state => !isEnabled(), + }), + WatchUi.SLIDE_LEFT + ); + return; + } + // System.println("HomeAssistantToggleMenuItem setState() URL = " + url); // System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id); Communications.makeWebRequest( @@ -302,5 +334,5 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { function onConfirm(b as Lang.Boolean) as Void { setState(b); } - + } diff --git a/source/Settings.mc b/source/Settings.mc index 658c9a8..19a732d 100644 --- a/source/Settings.mc +++ b/source/Settings.mc @@ -35,6 +35,7 @@ class Settings { private static var mCacheConfig as Lang.Boolean = false; private static var mClearCache as Lang.Boolean = false; private static var mVibrate as Lang.Boolean = false; + private static var mWifiLteExecution as Lang.Boolean = false; //! seconds private static var mAppTimeout as Lang.Number = 0; //! seconds @@ -69,6 +70,7 @@ class Settings { mMenuAlignment = Properties.getValue("menu_alignment"); mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level"); mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate"); + mWifiLteExecution = Properties.getValue("wifi_lte_execution"); } //! A webhook is required for non-privileged API calls. @@ -270,4 +272,12 @@ class Settings { } } + //! Get the value of the WiFi/LTE toggle in settings. + //! + //! @return The state of the toggle. + // + static function getWifiLteExecutionEnabled() as Lang.Boolean { + return mWifiLteExecution; + } + } diff --git a/source/WifiLteExecution.mc b/source/WifiLteExecution.mc new file mode 100644 index 0000000..48722dc --- /dev/null +++ b/source/WifiLteExecution.mc @@ -0,0 +1,236 @@ +using Toybox.WatchUi; +using Toybox.System; +using Toybox.Communications; +using Toybox.Lang; +using Toybox.Timer; + +// Delegate to respond to a confirmation to execute command via bulk sync +// +class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { + public static var mCommandData as { + :type as Lang.String, + :service as Lang.String or Null, + :data as Lang.Dictionary or Null, + :url as Lang.String or Null, + :id as Lang.Number or Null, + :exit as Lang.Boolean + }; + + private var mToggleMethod as Method(b as Lang.Boolean) as Void or Null; + private var mToggleState as Lang.Boolean or Null; + private var mHasToast as Lang.Boolean = false; + private var mTimer as Timer.Timer or Null; + + + //! Initializes a confirmation delegate to confirm a Wi-Fi or LTE command exection + //! + //! @param options A dictionary describing the command to be executed: + //! - type: The command type, either `"service"` or `"entity"`. + //! - service: (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on"). + //! - url: (For type `"entity"`) The full Home Assistant entity API URL. + //! - callback: (For type `"entity"`) A callback method (Method) to handle the response. + //! - data: (Optional) A dictionary of data to send with the request. + //! = exit: Boolean: true to exit after running command. + //! + //! @param toggleItem Optional toggle state information: + //! - confirmMethod: A method to call after confirmation. + //! - state: The state (boolean) that will be passed to the confirmMethod. + function initialize(cOptions as { + :type as Lang.String, + :service as Lang.String or Null, + :data as Lang.Dictionary or Null, + :url as Lang.String or Null, + :callback as Lang.Method or Null, + :exit as Lang.Boolean, + }, toggleItem as { + :confirmMethod as Lang.Method, + :state as Lang.Boolean + } or Null) { + if (WatchUi has :showToast) { + mHasToast = true; + } + + mCommandData = { + :type => cOptions[:type], + :service => cOptions[:service], + :data => cOptions[:data], + :url => cOptions[:url], + :callback => cOptions[:callback], + :exit => cOptions[:exit] + }; + if (toggleItem != null) { + mToggleMethod = toggleItem[:confirmMethod]; + mToggleState = toggleItem[:state]; + } + + var timeout = Settings.getConfirmTimeout(); // ms + if (timeout > 0) { + mTimer = new Timer.Timer(); + mTimer.start(method(:onTimeout), timeout, true); + } + + ConfirmationDelegate.initialize(); + } + + //! Handles the user's response to the confirmation dialog. + //! + //! @param response The user's confirmation response as `WatchUi.Confirm` + //! @return Always returns `true` to indicate the response was handled. + function onResponse(response) as Lang.Boolean { + if (response == WatchUi.CONFIRM_YES) { + if (mToggleMethod != null) { + mToggleMethod.invoke(mToggleState); + } + trySync(); + } + return true; + } + + //! Initiates a bulk sync process to execute a command, if connections are available + private function trySync() as Void { + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + var connectionInfo = System.getDeviceSettings().connectionInfo; + var keys = connectionInfo.keys(); + var possibleConnection = false; + + for(var i = 0; i < keys.size(); i++) { + if (keys[i] != :bluetooth) { + var connection = connectionInfo[keys[i]]; + if (connection.state != System.CONNECTION_STATE_NOT_INITIALIZED) { + possibleConnection = true; + break; + } + } + } + + if (possibleConnection) { + var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String; + Communications.startSync2({:message => syncString}); + } else { + var toast = WatchUi.loadResource($.Rez.Strings.WifiLteNotAvailable) as Lang.String; + if (mHasToast) { + WatchUi.showToast(toast, null); + } else { + new Alert({ + :timeout => Globals.scAlertTimeout, + :font => Graphics.FONT_MEDIUM, + :text => toast, + :fgcolor => Graphics.COLOR_WHITE, + :bgcolor => Graphics.COLOR_BLACK + }).pushView(WatchUi.SLIDE_IMMEDIATE); + } + } + } + + //! Function supplied to a timer in order to limit the time for which the confirmation can be provided. + function onTimeout() as Void { + mTimer.stop(); + WatchUi.popView(WatchUi.SLIDE_RIGHT); + } +} + +class HomeAssistantSyncDelegate extends Communications.SyncDelegate { + private static var syncError as Lang.String or Null; + + // Initialize an instance of this delegate + public function initialize() { + SyncDelegate.initialize(); + } + + //! Called by the system to determine if a sync is needed + public function isSyncNeeded() as Lang.Boolean { + return true; + } + + //! Called by the system when starting a bulk sync. + public function onStartSync() as Void { + syncError = null; + + if (WifiLteExecutionConfirmDelegate.mCommandData == null) { + syncError = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionDataError) as Lang.String; + onStopSync(); + return; + } + + var type = WifiLteExecutionConfirmDelegate.mCommandData[:type]; + var data = WifiLteExecutionConfirmDelegate.mCommandData[:data]; + var url; + + switch (type) { + case "service": + var service = WifiLteExecutionConfirmDelegate.mCommandData[:service]; + url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length()); + var entity_id = ""; + if (data != null) { + entity_id = data.get("entity_id"); + if (entity_id == null) { + entity_id = ""; + } + } + performRequest(url, data); + break; + case "entity": + url = WifiLteExecutionConfirmDelegate.mCommandData[:url]; + performRequest(url, data); + break; + } + } + + // Performs a POST request to Hass with a given payload and URL, and calls haCallback + private function performRequest(url as Lang.String, data as Lang.Dictionary or Null) { + Communications.makeWebRequest( + url, + data, // May include {"entity_id": xxxx} for service calls + { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, + "Authorization" => "Bearer " + Settings.getApiKey() + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }, + method(:haCallback) + ); + } + + //! Handle callback from request + public function haCallback(code as Lang.Number, data as Null or Lang.Dictionary) as Void { + if (code == 200) { + syncError = null; + if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) { + var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback]; + if (callbackMethod != null) { + var d = data as Lang.Array; + callbackMethod.invoke(d); + } + } + onStopSync(); + return; + } + + switch(code) { + case Communications.NETWORK_REQUEST_TIMED_OUT: + syncError = WatchUi.loadResource($.Rez.Strings.TimedOut) as Lang.String; + break; + case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE: + syncError = WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String; + syncError = ""; + default: + var codeMsg = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String; + syncError = codeMsg + code; + break; + } + + onStopSync(); + } + + //! Clean up + public function onStopSync() as Void { + if (WifiLteExecutionConfirmDelegate.mCommandData[:exit]) { + System.exit(); + } + + Communications.cancelAllRequests(); + Communications.notifySyncComplete(syncError); + } +} From b2b8ffb332eb9929cbd05087c6a70dd899d24adf Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Fri, 11 Jul 2025 14:15:55 +0100 Subject: [PATCH 02/11] separate syncdelegate class file, check for startsync2 --- source/HomeAssistantSyncDelegate.mc | 112 ++++++++++++++++ ....mc => WifiLteExecutionConfirmDelegate.mc} | 124 ++---------------- 2 files changed, 125 insertions(+), 111 deletions(-) create mode 100644 source/HomeAssistantSyncDelegate.mc rename source/{WifiLteExecution.mc => WifiLteExecutionConfirmDelegate.mc} (54%) diff --git a/source/HomeAssistantSyncDelegate.mc b/source/HomeAssistantSyncDelegate.mc new file mode 100644 index 0000000..f8ff6f6 --- /dev/null +++ b/source/HomeAssistantSyncDelegate.mc @@ -0,0 +1,112 @@ +using Toybox.Communications; +using Toybox.Lang; + +// SyncDelegate to execute single command via POST request to Home Assistant +// +class HomeAssistantSyncDelegate extends Communications.SyncDelegate { + private static var syncError as Lang.String or Null; + + // Initialize an instance of this delegate + public function initialize() { + SyncDelegate.initialize(); + } + + //! Called by the system to determine if a sync is needed + public function isSyncNeeded() as Lang.Boolean { + return true; + } + + //! Called by the system when starting a bulk sync. + public function onStartSync() as Void { + syncError = null; + + if (WifiLteExecutionConfirmDelegate.mCommandData == null) { + syncError = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionDataError) as Lang.String; + onStopSync(); + return; + } + + var type = WifiLteExecutionConfirmDelegate.mCommandData[:type]; + var data = WifiLteExecutionConfirmDelegate.mCommandData[:data]; + var url; + + switch (type) { + case "service": + var service = WifiLteExecutionConfirmDelegate.mCommandData[:service]; + url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length()); + var entity_id = ""; + if (data != null) { + entity_id = data.get("entity_id"); + if (entity_id == null) { + entity_id = ""; + } + } + performRequest(url, data); + break; + case "entity": + url = WifiLteExecutionConfirmDelegate.mCommandData[:url]; + performRequest(url, data); + break; + } + } + + // Performs a POST request to Hass with a given payload and URL, and calls haCallback + private function performRequest(url as Lang.String, data as Lang.Dictionary or Null) { + Communications.makeWebRequest( + url, + data, // May include {"entity_id": xxxx} for service calls + { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, + "Authorization" => "Bearer " + Settings.getApiKey() + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }, + method(:haCallback) + ); + } + + //! Handle callback from request + public function haCallback(code as Lang.Number, data as Null or Lang.Dictionary) as Void { + Communications.notifySyncProgress(100); + if (code == 200) { + syncError = null; + if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) { + var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback]; + if (callbackMethod != null) { + var d = data as Lang.Array; + callbackMethod.invoke(d); + } + } + onStopSync(); + return; + } + + switch(code) { + case Communications.NETWORK_REQUEST_TIMED_OUT: + syncError = WatchUi.loadResource($.Rez.Strings.TimedOut) as Lang.String; + break; + case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE: + syncError = WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String; + syncError = ""; + default: + var codeMsg = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String; + syncError = codeMsg + code; + break; + } + + onStopSync(); + } + + //! Clean up + public function onStopSync() as Void { + if (WifiLteExecutionConfirmDelegate.mCommandData[:exit]) { + System.exit(); + } + + Communications.cancelAllRequests(); + Communications.notifySyncComplete(syncError); + + } +} diff --git a/source/WifiLteExecution.mc b/source/WifiLteExecutionConfirmDelegate.mc similarity index 54% rename from source/WifiLteExecution.mc rename to source/WifiLteExecutionConfirmDelegate.mc index 48722dc..5477213 100644 --- a/source/WifiLteExecution.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -46,6 +46,8 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :confirmMethod as Lang.Method, :state as Lang.Boolean } or Null) { + ConfirmationDelegate.initialize(); + if (WatchUi has :showToast) { mHasToast = true; } @@ -68,8 +70,6 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { mTimer = new Timer.Timer(); mTimer.start(method(:onTimeout), timeout, true); } - - ConfirmationDelegate.initialize(); } //! Handles the user's response to the confirmation dialog. @@ -77,6 +77,10 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! @param response The user's confirmation response as `WatchUi.Confirm` //! @return Always returns `true` to indicate the response was handled. function onResponse(response) as Lang.Boolean { + if (mTimer != null) { + mTimer.stop(); + } + if (response == WatchUi.CONFIRM_YES) { if (mToggleMethod != null) { mToggleMethod.invoke(mToggleState); @@ -104,8 +108,12 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { } if (possibleConnection) { - var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String; - Communications.startSync2({:message => syncString}); + if (Communications has :startSync2) { + var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String; + Communications.startSync2({:message => syncString}); + } else { + Communications.startSync(); + } } else { var toast = WatchUi.loadResource($.Rez.Strings.WifiLteNotAvailable) as Lang.String; if (mHasToast) { @@ -127,110 +135,4 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { mTimer.stop(); WatchUi.popView(WatchUi.SLIDE_RIGHT); } -} - -class HomeAssistantSyncDelegate extends Communications.SyncDelegate { - private static var syncError as Lang.String or Null; - - // Initialize an instance of this delegate - public function initialize() { - SyncDelegate.initialize(); - } - - //! Called by the system to determine if a sync is needed - public function isSyncNeeded() as Lang.Boolean { - return true; - } - - //! Called by the system when starting a bulk sync. - public function onStartSync() as Void { - syncError = null; - - if (WifiLteExecutionConfirmDelegate.mCommandData == null) { - syncError = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionDataError) as Lang.String; - onStopSync(); - return; - } - - var type = WifiLteExecutionConfirmDelegate.mCommandData[:type]; - var data = WifiLteExecutionConfirmDelegate.mCommandData[:data]; - var url; - - switch (type) { - case "service": - var service = WifiLteExecutionConfirmDelegate.mCommandData[:service]; - url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length()); - var entity_id = ""; - if (data != null) { - entity_id = data.get("entity_id"); - if (entity_id == null) { - entity_id = ""; - } - } - performRequest(url, data); - break; - case "entity": - url = WifiLteExecutionConfirmDelegate.mCommandData[:url]; - performRequest(url, data); - break; - } - } - - // Performs a POST request to Hass with a given payload and URL, and calls haCallback - private function performRequest(url as Lang.String, data as Lang.Dictionary or Null) { - Communications.makeWebRequest( - url, - data, // May include {"entity_id": xxxx} for service calls - { - :method => Communications.HTTP_REQUEST_METHOD_POST, - :headers => { - "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, - "Authorization" => "Bearer " + Settings.getApiKey() - }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON - }, - method(:haCallback) - ); - } - - //! Handle callback from request - public function haCallback(code as Lang.Number, data as Null or Lang.Dictionary) as Void { - if (code == 200) { - syncError = null; - if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) { - var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback]; - if (callbackMethod != null) { - var d = data as Lang.Array; - callbackMethod.invoke(d); - } - } - onStopSync(); - return; - } - - switch(code) { - case Communications.NETWORK_REQUEST_TIMED_OUT: - syncError = WatchUi.loadResource($.Rez.Strings.TimedOut) as Lang.String; - break; - case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE: - syncError = WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String; - syncError = ""; - default: - var codeMsg = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String; - syncError = codeMsg + code; - break; - } - - onStopSync(); - } - - //! Clean up - public function onStopSync() as Void { - if (WifiLteExecutionConfirmDelegate.mCommandData[:exit]) { - System.exit(); - } - - Communications.cancelAllRequests(); - Communications.notifySyncComplete(syncError); - } -} +} \ No newline at end of file From 62f0e711c97fcce3e31dcaf776cbf59aabca291f Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Fri, 11 Jul 2025 17:11:44 +0100 Subject: [PATCH 03/11] make setting conditional by using group --- resources/settings/settings.xml | 14 ++++++++------ resources/strings/strings.xml | 4 +++- source/Settings.mc | 4 ++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml index 17552bb..b0dabe4 100644 --- a/resources/settings/settings.xml +++ b/resources/settings/settings.xml @@ -117,10 +117,12 @@ - - - + + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index ec32446..bf5cede 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -70,5 +70,7 @@ Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant. The refresh rate (in minutes) at which the background service should repeat sending data. (Read only) The Webhook ID created by the device for background service updates. You might require this for debugging. - Allows the app to start without phone connection, and prompt to execute command over Wifi/LTE. + Wifi/LTE execution mode. + Enable executing commands over Wifi/LTE. + Allows the app to start without phone connection (when menu is cached), and prompt to execute command over Wifi/LTE. diff --git a/source/Settings.mc b/source/Settings.mc index 19a732d..9075198 100644 --- a/source/Settings.mc +++ b/source/Settings.mc @@ -277,6 +277,10 @@ class Settings { //! @return The state of the toggle. // static function getWifiLteExecutionEnabled() as Lang.Boolean { + // Wifi/LTE sync execution on a cached menu + if (!mCacheConfig) { + return false; + } return mWifiLteExecution; } From b45f02ef7ba8ff03d6599a5b9369dfda13dfc050 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Fri, 11 Jul 2025 19:40:44 +0100 Subject: [PATCH 04/11] move popview up so it does not close wifi dialog --- source/HomeAssistantPinConfirmation.mc | 2 +- source/HomeAssistantToggleMenuItem.mc | 4 ++-- source/WifiLteExecutionConfirmDelegate.mc | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/HomeAssistantPinConfirmation.mc b/source/HomeAssistantPinConfirmation.mc index 436dd39..0a01b99 100644 --- a/source/HomeAssistantPinConfirmation.mc +++ b/source/HomeAssistantPinConfirmation.mc @@ -237,8 +237,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { if (mTimer != null) { mTimer.stop(); } - mConfirmMethod.invoke(mState); WatchUi.popView(WatchUi.SLIDE_RIGHT); + mConfirmMethod.invoke(mState); } else { error(); } diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index e3bf4a9..be2e51c 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -256,7 +256,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { url = url + id.substring(0, id.find(".")) + "/turn_off"; } - if (! phoneConnected && ! internetAvailable && Settings.getWifiLteExecutionEnabled()) { + if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) { var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; var dialog = new WatchUi.Confirmation(dialogMsg); WatchUi.pushView( @@ -334,5 +334,5 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { function onConfirm(b as Lang.Boolean) as Void { setState(b); } - + } diff --git a/source/WifiLteExecutionConfirmDelegate.mc b/source/WifiLteExecutionConfirmDelegate.mc index 5477213..0a4b5f4 100644 --- a/source/WifiLteExecutionConfirmDelegate.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -47,7 +47,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :state as Lang.Boolean } or Null) { ConfirmationDelegate.initialize(); - + if (WatchUi has :showToast) { mHasToast = true; } @@ -77,6 +77,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! @param response The user's confirmation response as `WatchUi.Confirm` //! @return Always returns `true` to indicate the response was handled. function onResponse(response) as Lang.Boolean { + getApp().getQuitTimer().reset(); if (mTimer != null) { mTimer.stop(); } From ac899ff78420c8cbcab89819f4dd7e8b971b9961 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Fri, 11 Jul 2025 22:44:26 +0100 Subject: [PATCH 05/11] simplify delegate, toggle switch for empty response body --- source/HomeAssistantService.mc | 2 +- source/HomeAssistantSyncDelegate.mc | 5 ++--- source/HomeAssistantToggleMenuItem.mc | 22 +++++++++++++--------- source/WifiLteExecutionConfirmDelegate.mc | 20 +------------------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc index eca2509..53dbf80 100644 --- a/source/HomeAssistantService.mc +++ b/source/HomeAssistantService.mc @@ -141,7 +141,7 @@ class HomeAssistantService { :service => service, :data => data, :exit => exit, - }, null), + }), WatchUi.SLIDE_LEFT ); } else if (! phoneConnected) { diff --git a/source/HomeAssistantSyncDelegate.mc b/source/HomeAssistantSyncDelegate.mc index f8ff6f6..92161d8 100644 --- a/source/HomeAssistantSyncDelegate.mc +++ b/source/HomeAssistantSyncDelegate.mc @@ -61,7 +61,7 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate { "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, "Authorization" => "Bearer " + Settings.getApiKey() }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON, }, method(:haCallback) ); @@ -73,7 +73,7 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate { if (code == 200) { syncError = null; if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) { - var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback]; + var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback]; if (callbackMethod != null) { var d = data as Lang.Array; callbackMethod.invoke(d); @@ -107,6 +107,5 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate { Communications.cancelAllRequests(); Communications.notifySyncComplete(syncError); - } } diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index be2e51c..6554598 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -218,12 +218,19 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { //! @param data An array of dictionaries, each representing a Home Assistant entity state. // function setToggleStateWithData(data as Lang.Array) { - for(var i = 0; i < data.size(); i++) { - if ((data[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) { - var state = data[i].get("state") as Lang.String; - // System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); - setUiToggle(state); - WatchUi.requestUpdate(); + // if there's no response body, let's assume that what we did, happened, and flip the toggle + if (data.size() == 0) { + setEnabled(!isEnabled()); + } + + else { + for(var i = 0; i < data.size(); i++) { + if ((data[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) { + var state = data[i].get("state") as Lang.String; + // System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); + setUiToggle(state); + WatchUi.requestUpdate(); + } } } } @@ -268,9 +275,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { :data => mData, :callback => method(:setToggleStateWithData), :exit => mExit, - }, { - :confirmMethod => method(:onConfirm), - :state => !isEnabled(), }), WatchUi.SLIDE_LEFT ); diff --git a/source/WifiLteExecutionConfirmDelegate.mc b/source/WifiLteExecutionConfirmDelegate.mc index 0a4b5f4..fc95304 100644 --- a/source/WifiLteExecutionConfirmDelegate.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -16,12 +16,9 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :exit as Lang.Boolean }; - private var mToggleMethod as Method(b as Lang.Boolean) as Void or Null; - private var mToggleState as Lang.Boolean or Null; private var mHasToast as Lang.Boolean = false; private var mTimer as Timer.Timer or Null; - //! Initializes a confirmation delegate to confirm a Wi-Fi or LTE command exection //! //! @param options A dictionary describing the command to be executed: @@ -31,10 +28,6 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! - callback: (For type `"entity"`) A callback method (Method) to handle the response. //! - data: (Optional) A dictionary of data to send with the request. //! = exit: Boolean: true to exit after running command. - //! - //! @param toggleItem Optional toggle state information: - //! - confirmMethod: A method to call after confirmation. - //! - state: The state (boolean) that will be passed to the confirmMethod. function initialize(cOptions as { :type as Lang.String, :service as Lang.String or Null, @@ -42,10 +35,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :url as Lang.String or Null, :callback as Lang.Method or Null, :exit as Lang.Boolean, - }, toggleItem as { - :confirmMethod as Lang.Method, - :state as Lang.Boolean - } or Null) { + }) { ConfirmationDelegate.initialize(); if (WatchUi has :showToast) { @@ -60,10 +50,6 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :callback => cOptions[:callback], :exit => cOptions[:exit] }; - if (toggleItem != null) { - mToggleMethod = toggleItem[:confirmMethod]; - mToggleState = toggleItem[:state]; - } var timeout = Settings.getConfirmTimeout(); // ms if (timeout > 0) { @@ -83,9 +69,6 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { } if (response == WatchUi.CONFIRM_YES) { - if (mToggleMethod != null) { - mToggleMethod.invoke(mToggleState); - } trySync(); } return true; @@ -93,7 +76,6 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! Initiates a bulk sync process to execute a command, if connections are available private function trySync() as Void { - WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); var connectionInfo = System.getDeviceSettings().connectionInfo; var keys = connectionInfo.keys(); var possibleConnection = false; From 979d85fce5e328e570093b6a18d496e4b953afdf Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Sat, 12 Jul 2025 00:03:09 +0100 Subject: [PATCH 06/11] show toast, not error, in updateMenuItems --- source/HomeAssistantApp.mc | 30 ++++++++++++++++++++++++++++-- source/HomeAssistantService.mc | 5 ++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 5f0324e..ceb4348 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -26,6 +26,7 @@ using Toybox.Timer; (:glance, :background) class HomeAssistantApp extends Application.AppBase { private var mApiStatus as Lang.String or Null; + private var mHasToast as Lang.Boolean = false; private var mMenuStatus as Lang.String or Null; private var mHaMenu as HomeAssistantView or Null; private var mGlanceTemplate as Lang.String or Null = null; @@ -106,6 +107,7 @@ class HomeAssistantApp extends Application.AppBase { 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) { @@ -428,11 +430,35 @@ class HomeAssistantApp extends Application.AppBase { //! Construct the GET request to update all menu items. // function updateMenuItems() as Void { - if (! System.getDeviceSettings().phoneConnected) { + var phoneConnected = System.getDeviceSettings().phoneConnected; + var connectionAvailable = System.getDeviceSettings().connectionAvailable; + + // In Wifi/LTE execution mode, we should not show an error page but use a toast instead. + if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) { + 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.scAlertTimeout, + :font => Graphics.FONT_MEDIUM, + :text => toast, + :fgcolor => Graphics.COLOR_WHITE, + :bgcolor => Graphics.COLOR_BLACK + }).pushView(WatchUi.SLIDE_IMMEDIATE); + } + 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 (! System.getDeviceSettings().connectionAvailable) { + } 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); diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc index 53dbf80..c69b6f9 100644 --- a/source/HomeAssistantService.mc +++ b/source/HomeAssistantService.mc @@ -27,9 +27,8 @@ class HomeAssistantService { //! Class Constructor // function initialize() { - if (WatchUi has :showToast) { - mHasToast = true; - } + mHasToast = WatchUi has :showToast; + if (Attention has :vibrate) { mHasVibrate = true; } From 576f8c4a641ef58935e40bc3b1915272d36a7171 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Sat, 12 Jul 2025 16:52:38 +0100 Subject: [PATCH 07/11] HomeAssistantConfirmationDelegate and HomeAssistantPinConfirmationDelegate undo toggles on timeout and reject --- source/HomeAssistantConfirmation.mc | 27 ++++++++++++++-- source/HomeAssistantPinConfirmation.mc | 38 ++++++++++++++--------- source/HomeAssistantTapMenuItem.mc | 12 +++++-- source/HomeAssistantToggleMenuItem.mc | 14 +++++++-- source/WifiLteExecutionConfirmDelegate.mc | 12 +++---- 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/source/HomeAssistantConfirmation.mc b/source/HomeAssistantConfirmation.mc index 9b48714..a59829f 100644 --- a/source/HomeAssistantConfirmation.mc +++ b/source/HomeAssistantConfirmation.mc @@ -38,13 +38,25 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { private var mConfirmMethod as Method(state as Lang.Boolean) as Void; private var mTimer as Timer.Timer or Null; private var mState as Lang.Boolean; + private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null; //! Class Constructor + //! + //! @param options A dictionary describing the following options: + //! - callback Method to call on confirmation. + //! - state Wanted state of a toggle button. + //! - toggle Optional setEnabled method to untoggle ToggleItem. // - function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) { + function initialize(options as { + :callback as Method(state as Lang.Boolean) as Void, + :state as Lang.Boolean, + :toggleMethod as Method(state as Lang.Boolean) or Null, + }) { WatchUi.ConfirmationDelegate.initialize(); - mConfirmMethod = callback; - mState = state; + mConfirmMethod = options[:callback]; + mState = options[:state]; + mToggleMethod = options[:toggleMethod]; + var timeout = Settings.getConfirmTimeout(); // ms if (timeout > 0) { mTimer = new Timer.Timer(); @@ -64,6 +76,11 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { } if (response == WatchUi.CONFIRM_YES) { mConfirmMethod.invoke(mState); + } else { + // Undo the toggle, if we have one + if (mToggleMethod != null) { + mToggleMethod.invoke(!mState); + } } return true; } @@ -71,6 +88,10 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { //! Function supplied to a timer in order to limit the time for which the confirmation can be provided. function onTimeout() as Void { mTimer.stop(); + // Undo the toggle, if we have one + if (mToggleMethod != null) { + mToggleMethod.invoke(!mState); + } WatchUi.popView(WatchUi.SLIDE_RIGHT); } } diff --git a/source/HomeAssistantPinConfirmation.mc b/source/HomeAssistantPinConfirmation.mc index 0a01b99..2cb7501 100644 --- a/source/HomeAssistantPinConfirmation.mc +++ b/source/HomeAssistantPinConfirmation.mc @@ -185,21 +185,25 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { private var mTimer as Timer.Timer or Null; private var mState as Lang.Boolean; private var mFailures as PinFailures; + private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null; private var mView as HomeAssistantPinConfirmationView; //! Class Constructor //! - //! @param callback Method to call on confirmation. - //! @param state Current state of a toggle button. - //! @param pin PIN to be matched. - //! @param view PIN confirmation view. + //! @param options A dictionary describing the following options: + //! - callback Method to call on confirmation. + //! - pin PIN to be matched. + //! - state Wanted state of a toggle button. + //! - toggle Optional setEnabled method to untoggle ToggleItem. + //! - view PIN confirmation view. // - function initialize( - callback as Method(state as Lang.Boolean) as Void, - state as Lang.Boolean, - pin as Lang.String, - view as HomeAssistantPinConfirmationView - ) { + function initialize(options as { + :callback as Method(state as Lang.Boolean) as Void, + :pin as Lang.String, + :state as Lang.Boolean, + :view as HomeAssistantPinConfirmationView, + :toggleMethod as (Method(state as Lang.Boolean) as Void) or Null, + }) { BehaviorDelegate.initialize(); mFailures = new PinFailures(); if (mFailures.isLocked()) { @@ -208,11 +212,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { WatchUi.loadResource($.Rez.Strings.Seconds); WatchUi.showToast(msg, {}); } - mPin = pin; + mPin = options[:pin]; mEnteredPin = ""; - mConfirmMethod = callback; - mState = state; - mView = view; + mConfirmMethod = options[:callback]; + mState = options[:state]; + mToggleMethod = options[:toggleMethod]; + mView = options[:view]; + resetTimer(); } @@ -279,6 +285,10 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { if (mTimer != null) { mTimer.stop(); } + // Undo the toggle, if we have one + if (mToggleMethod != null) { + mToggleMethod.invoke(!mState); + } WatchUi.popView(WatchUi.SLIDE_RIGHT); } diff --git a/source/HomeAssistantTapMenuItem.mc b/source/HomeAssistantTapMenuItem.mc index 88aabaa..0b57c25 100644 --- a/source/HomeAssistantTapMenuItem.mc +++ b/source/HomeAssistantTapMenuItem.mc @@ -82,14 +82,22 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { var pinConfirmationView = new HomeAssistantPinConfirmationView(); WatchUi.pushView( pinConfirmationView, - new HomeAssistantPinConfirmationDelegate(method(:onConfirm), false, pin, pinConfirmationView), + new HomeAssistantPinConfirmationDelegate({ + :callback => method(:onConfirm), + :pin => pin, + :state => false, + :view => pinConfirmationView, + }), WatchUi.SLIDE_IMMEDIATE ); } } else if (mConfirm) { WatchUi.pushView( new HomeAssistantConfirmation(), - new HomeAssistantConfirmationDelegate(method(:onConfirm), false), + new HomeAssistantConfirmationDelegate({ + :callback => method(:onConfirm), + :state => false, + }), WatchUi.SLIDE_IMMEDIATE ); } else { diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 6554598..7a41b4d 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -316,14 +316,24 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { var pinConfirmationView = new HomeAssistantPinConfirmationView(); WatchUi.pushView( pinConfirmationView, - new HomeAssistantPinConfirmationDelegate(method(:onConfirm), b, pin, pinConfirmationView), + new HomeAssistantPinConfirmationDelegate({ + :callback => method(:onConfirm), + :pin => pin, + :state => b, + :toggleMethod => method(:setEnabled), + :view => pinConfirmationView, + }), WatchUi.SLIDE_IMMEDIATE ); } } else if (mConfirm) { WatchUi.pushView( new HomeAssistantConfirmation(), - new HomeAssistantConfirmationDelegate(method(:onConfirm), b), + new HomeAssistantConfirmationDelegate({ + :callback => method(:onConfirm), + :state => b, + :toggleMethod => method(:setEnabled), + }), WatchUi.SLIDE_IMMEDIATE ); } else { diff --git a/source/WifiLteExecutionConfirmDelegate.mc b/source/WifiLteExecutionConfirmDelegate.mc index fc95304..1901403 100644 --- a/source/WifiLteExecutionConfirmDelegate.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -43,12 +43,12 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { } mCommandData = { - :type => cOptions[:type], - :service => cOptions[:service], - :data => cOptions[:data], - :url => cOptions[:url], - :callback => cOptions[:callback], - :exit => cOptions[:exit] + :type => cOptions[:type], + :service => cOptions[:service], + :data => cOptions[:data], + :url => cOptions[:url], + :callback => cOptions[:callback], + :exit => cOptions[:exit] }; var timeout = Settings.getConfirmTimeout(); // ms From be7eed1ae1733a9e1d45a890e1bd314a2ea19578 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Thu, 17 Jul 2025 23:31:05 +0100 Subject: [PATCH 08/11] early return from fetchApiStatus for in-app wifi, fix typo in docstrings --- source/HomeAssistantApp.mc | 11 +++++++++-- source/WifiLteExecutionConfirmDelegate.mc | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index ceb4348..ba2471c 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -575,20 +575,27 @@ class HomeAssistantApp extends Application.AppBase { // (:glance) 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 (! System.getDeviceSettings().phoneConnected) { + if (! mIsGlance && 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 (mIsGlance) { WatchUi.requestUpdate(); } else { + System.println("we here"); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); } - } else if (! System.getDeviceSettings().connectionAvailable) { + } else if (! connectionAvailable) { // System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call."); mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String; if (mIsGlance) { diff --git a/source/WifiLteExecutionConfirmDelegate.mc b/source/WifiLteExecutionConfirmDelegate.mc index 1901403..a2cdd8e 100644 --- a/source/WifiLteExecutionConfirmDelegate.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -27,7 +27,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! - url: (For type `"entity"`) The full Home Assistant entity API URL. //! - callback: (For type `"entity"`) A callback method (Method) to handle the response. //! - data: (Optional) A dictionary of data to send with the request. - //! = exit: Boolean: true to exit after running command. + //! - exit: Boolean: if set to true: exit after running command. function initialize(cOptions as { :type as Lang.String, :service as Lang.String or Null, From db3fbd988637737ddf8d3c74b93d696085883595 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Sat, 19 Jul 2025 00:38:38 +0100 Subject: [PATCH 09/11] timers as statics, defensive popviews, no double confirmation, add pin screen onBack, toggle state tweaks --- source/HomeAssistantConfirmation.mc | 34 +++++--- source/HomeAssistantPinConfirmation.mc | 17 +++- source/HomeAssistantService.mc | 2 +- source/HomeAssistantTapMenuItem.mc | 35 ++++++-- source/HomeAssistantToggleMenuItem.mc | 99 +++++++++++++++-------- source/WifiLteExecutionConfirmDelegate.mc | 22 ++++- 6 files changed, 150 insertions(+), 59 deletions(-) diff --git a/source/HomeAssistantConfirmation.mc b/source/HomeAssistantConfirmation.mc index a59829f..dab35e2 100644 --- a/source/HomeAssistantConfirmation.mc +++ b/source/HomeAssistantConfirmation.mc @@ -35,31 +35,43 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation { //! Delegate to respond to the confirmation request. // class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { - private var mConfirmMethod as Method(state as Lang.Boolean) as Void; - private var mTimer as Timer.Timer or Null; - private var mState as Lang.Boolean; - private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null; + private static var mTimer as Timer.Timer or Null; + + private var mConfirmMethod as Method(state as Lang.Boolean) as Void; + private var mState as Lang.Boolean; + private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null; + private var mConfirmationView as WatchUi.Confirmation; //! Class Constructor //! //! @param options A dictionary describing the following options: - //! - callback Method to call on confirmation. - //! - state Wanted state of a toggle button. - //! - toggle Optional setEnabled method to untoggle ToggleItem. + //! - callback Method to call on confirmation. + //! - confirmationView Confirmation the delegate is active for + //! - state Wanted state of a toggle button. + //! - toggle Optional setEnabled method to untoggle ToggleItem. // function initialize(options as { :callback as Method(state as Lang.Boolean) as Void, + :confirmationView as WatchUi.Confirmation, :state as Lang.Boolean, :toggleMethod as Method(state as Lang.Boolean) or Null, }) { + if (mTimer != null) { + mTimer.stop(); + } + WatchUi.ConfirmationDelegate.initialize(); mConfirmMethod = options[:callback]; + mConfirmationView = options[:confirmationView]; mState = options[:state]; mToggleMethod = options[:toggleMethod]; var timeout = Settings.getConfirmTimeout(); // ms if (timeout > 0) { - mTimer = new Timer.Timer(); + if (mTimer == null) { + mTimer = new Timer.Timer(); + } + mTimer.start(method(:onTimeout), timeout, true); } } @@ -92,6 +104,10 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { if (mToggleMethod != null) { mToggleMethod.invoke(!mState); } - WatchUi.popView(WatchUi.SLIDE_RIGHT); + + var getCurrentView = WatchUi.getCurrentView(); + if (getCurrentView[0] == mConfirmationView) { + WatchUi.popView(WatchUi.SLIDE_RIGHT); + } } } diff --git a/source/HomeAssistantPinConfirmation.mc b/source/HomeAssistantPinConfirmation.mc index 2cb7501..5ccf475 100644 --- a/source/HomeAssistantPinConfirmation.mc +++ b/source/HomeAssistantPinConfirmation.mc @@ -244,6 +244,11 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { mTimer.stop(); } WatchUi.popView(WatchUi.SLIDE_RIGHT); + + // Set the toggle, if we have one + if (mToggleMethod != null) { + mToggleMethod.invoke(!mState); + } mConfirmMethod.invoke(mState); } else { error(); @@ -285,10 +290,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { if (mTimer != null) { mTimer.stop(); } - // Undo the toggle, if we have one - if (mToggleMethod != null) { - mToggleMethod.invoke(!mState); - } + WatchUi.popView(WatchUi.SLIDE_RIGHT); } @@ -314,6 +316,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { goBack(); } + //! Handle the back button (ESC) + // + function onBack() as Lang.Boolean { + goBack(); + return true; + } + } diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc index c69b6f9..56f3f9f 100644 --- a/source/HomeAssistantService.mc +++ b/source/HomeAssistantService.mc @@ -140,7 +140,7 @@ class HomeAssistantService { :service => service, :data => data, :exit => exit, - }), + }, dialog), WatchUi.SLIDE_LEFT ); } else if (! phoneConnected) { diff --git a/source/HomeAssistantTapMenuItem.mc b/source/HomeAssistantTapMenuItem.mc index 0b57c25..8d22d81 100644 --- a/source/HomeAssistantTapMenuItem.mc +++ b/source/HomeAssistantTapMenuItem.mc @@ -92,14 +92,33 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { ); } } else if (mConfirm) { - WatchUi.pushView( - new HomeAssistantConfirmation(), - new HomeAssistantConfirmationDelegate({ - :callback => method(:onConfirm), - :state => false, - }), - WatchUi.SLIDE_IMMEDIATE - ); + var phoneConnected = System.getDeviceSettings().phoneConnected; + var internetAvailable = System.getDeviceSettings().connectionAvailable; + if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) { + var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; + var dialog = new WatchUi.Confirmation(dialogMsg); + WatchUi.pushView( + dialog, + new WifiLteExecutionConfirmDelegate({ + :type => "service", + :service => mService, + :data => mData, + :exit => mExit, + }, dialog), + WatchUi.SLIDE_LEFT + ); + } else { + var view = new HomeAssistantConfirmation(); + WatchUi.pushView( + view, + new HomeAssistantConfirmationDelegate({ + :callback => method(:onConfirm), + :confirmationView => view, + :state => false, + }), + WatchUi.SLIDE_IMMEDIATE + ); + } } else { onConfirm(false); } diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 7a41b4d..3c22ed0 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -240,10 +240,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { //! @param s Boolean indicating the desired state of the toggle switch. // function setState(s as Lang.Boolean) as Void { - // Toggle the UI back, we'll wait for confirmation from the Home Assistant - // Note: with Zigbee2MQTT a.o. we may not always get the state in the response. - setEnabled(!isEnabled()); - var phoneConnected = System.getDeviceSettings().phoneConnected; var internetAvailable = System.getDeviceSettings().connectionAvailable; @@ -256,28 +252,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); } else { var id = mData.get("entity_id") as Lang.String; - var url = Settings.getApiUrl() + "/services/"; - if (s) { - url = url + id.substring(0, id.find(".")) + "/turn_on"; - } else { - url = url + id.substring(0, id.find(".")) + "/turn_off"; - } + var url = getUrl(id, s); if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) { - var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; - var dialog = new WatchUi.Confirmation(dialogMsg); - WatchUi.pushView( - dialog, - new WifiLteExecutionConfirmDelegate({ - :type => "entity", - :url => url, - :id => id, - :data => mData, - :callback => method(:setToggleStateWithData), - :exit => mExit, - }), - WatchUi.SLIDE_LEFT - ); + wifiPrompt(s); return; } @@ -311,6 +289,9 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { function callService(b as Lang.Boolean) as Void { var hasTouchScreen = System.getDeviceSettings().isTouchScreen; if (mPin && hasTouchScreen) { + // Undo the toggle + setEnabled(!isEnabled()); + var pin = Settings.getPin(); if (pin != null) { var pinConfirmationView = new HomeAssistantPinConfirmationView(); @@ -327,15 +308,26 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { ); } } else if (mConfirm) { - WatchUi.pushView( - new HomeAssistantConfirmation(), - new HomeAssistantConfirmationDelegate({ - :callback => method(:onConfirm), - :state => b, - :toggleMethod => method(:setEnabled), - }), - WatchUi.SLIDE_IMMEDIATE - ); + // Undo the toggle + setEnabled(!isEnabled()); + + var phoneConnected = System.getDeviceSettings().phoneConnected; + var internetAvailable = System.getDeviceSettings().connectionAvailable; + if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) { + wifiPrompt(b); + } else { + var confirmationView = new HomeAssistantConfirmation(); + WatchUi.pushView( + confirmationView, + new HomeAssistantConfirmationDelegate({ + :callback => method(:onConfirm), + :confirmationView => confirmationView, + :state => b, + :toggleMethod => method(:setEnabled), + }), + WatchUi.SLIDE_IMMEDIATE + ); + } } else { onConfirm(b); } @@ -349,4 +341,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { setState(b); } + //! Displays a confirmation dialog before executing a service call via Wi-Fi/LTE. + //! + //! @param s Desired state: `true` to turn on, `false` to turn off. + // + private function wifiPrompt(s as Lang.Boolean) as Void { + var id = mData.get("entity_id") as Lang.String; + var url = getUrl(id, s); + + var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; + var dialog = new WatchUi.Confirmation(dialogMsg); + WatchUi.pushView( + dialog, + new WifiLteExecutionConfirmDelegate({ + :type => "entity", + :url => url, + :id => id, + :data => mData, + :callback => method(:setToggleStateWithData), + :exit => mExit, + }, dialog), + WatchUi.SLIDE_LEFT + ); + } + + //! Constructs a Home Assistant API URL for the given entity and desired state. + //! + //! @param id The entity ID, e.g., `"switch.kitchen"`. + //! @param s Desired state: `true` for "turn_on", `false` for "turn_off". + //! + //! @return Full service URL string. + // + private function getUrl(id as Lang.String, s as Lang.Boolean) as Lang.String { + var url = Settings.getApiUrl() + "/services/"; + if (s) { + url = url + id.substring(0, id.find(".")) + "/turn_on"; + } else { + url = url + id.substring(0, id.find(".")) + "/turn_off"; + } + + return url; + } } diff --git a/source/WifiLteExecutionConfirmDelegate.mc b/source/WifiLteExecutionConfirmDelegate.mc index a2cdd8e..563baf6 100644 --- a/source/WifiLteExecutionConfirmDelegate.mc +++ b/source/WifiLteExecutionConfirmDelegate.mc @@ -16,8 +16,9 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :exit as Lang.Boolean }; + private static var mTimer as Timer.Timer or Null; private var mHasToast as Lang.Boolean = false; - private var mTimer as Timer.Timer or Null; + private var mConfirmationView as WatchUi.Confirmation; //! Initializes a confirmation delegate to confirm a Wi-Fi or LTE command exection //! @@ -28,6 +29,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! - callback: (For type `"entity"`) A callback method (Method) to handle the response. //! - data: (Optional) A dictionary of data to send with the request. //! - exit: Boolean: if set to true: exit after running command. + //! @param view The Confirmation view the delegate is active for function initialize(cOptions as { :type as Lang.String, :service as Lang.String or Null, @@ -35,13 +37,18 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { :url as Lang.String or Null, :callback as Lang.Method or Null, :exit as Lang.Boolean, - }) { + }, view as WatchUi.Confirmation) { ConfirmationDelegate.initialize(); + if (mTimer != null) { + mTimer.stop(); + } + if (WatchUi has :showToast) { mHasToast = true; } + mConfirmationView = view; mCommandData = { :type => cOptions[:type], :service => cOptions[:service], @@ -53,7 +60,10 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { var timeout = Settings.getConfirmTimeout(); // ms if (timeout > 0) { - mTimer = new Timer.Timer(); + if (mTimer == null) { + mTimer = new Timer.Timer(); + } + mTimer.start(method(:onTimeout), timeout, true); } } @@ -116,6 +126,10 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { //! Function supplied to a timer in order to limit the time for which the confirmation can be provided. function onTimeout() as Void { mTimer.stop(); - WatchUi.popView(WatchUi.SLIDE_RIGHT); + var getCurrentView = WatchUi.getCurrentView(); + + if (getCurrentView[0] == mConfirmationView) { + WatchUi.popView(WatchUi.SLIDE_RIGHT); + } } } \ No newline at end of file From d32135af6362419d814e1c35d70f9cd8432cb5e7 Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Mon, 21 Jul 2025 19:45:14 +0100 Subject: [PATCH 10/11] fix setting toggleItem, periodic pulling --- source/HomeAssistantApp.mc | 46 ++++++++++++++++++--------- source/HomeAssistantToggleMenuItem.mc | 2 ++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index ba2471c..dcedea8 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -40,6 +40,7 @@ class HomeAssistantApp extends Application.AppBase { 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 = {}; + private var mNotifiedNoBle as Lang.Boolean = false; //! Class Constructor // @@ -321,11 +322,11 @@ class HomeAssistantApp extends Application.AppBase { //! Start the periodic menu updates for as long as the application is running. // - function startUpdates() { + function startUpdates() as Void { if (mHaMenu != null and !mUpdating) { // Start the continuous update process that continues for as long as the application is running. - updateMenuItems(); mUpdating = true; + updateMenuItems(); } } @@ -435,22 +436,35 @@ class HomeAssistantApp extends Application.AppBase { // In Wifi/LTE execution mode, we should not show an error page but use a toast instead. if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) { - var toast = WatchUi.loadResource($.Rez.Strings.NoPhone); - if (!connectionAvailable) { - toast = WatchUi.loadResource($.Rez.Strings.NoInternet); + // 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.scAlertTimeout, + :font => Graphics.FONT_MEDIUM, + :text => toast, + :fgcolor => Graphics.COLOR_WHITE, + :bgcolor => Graphics.COLOR_BLACK + }).pushView(WatchUi.SLIDE_IMMEDIATE); + } } - if (mHasToast) { - WatchUi.showToast(toast, null); - } else { - new Alert({ - :timeout => Globals.scAlertTimeout, - :font => Graphics.FONT_MEDIUM, - :text => toast, - :fgcolor => Graphics.COLOR_WHITE, - :bgcolor => Graphics.COLOR_BLACK - }).pushView(WatchUi.SLIDE_IMMEDIATE); + mNotifiedNoBle = true; + setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); + + var delay = Settings.getPollDelay(); + if (delay > 0) { + mUpdateTimer.start(method(:startUpdates), delay, false); } + + mUpdating = false; return; } @@ -463,6 +477,8 @@ class HomeAssistantApp extends Application.AppBase { 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 = {}; diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 3c22ed0..2c47de2 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -255,6 +255,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { var url = getUrl(id, s); if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) { + // Undo the toggle + setEnabled(!isEnabled()); wifiPrompt(s); return; } From 0b84983eaf4ce570b8251a8d2428ac8ce22bbece Mon Sep 17 00:00:00 2001 From: Vincent Elger Zwanenburg Date: Mon, 21 Jul 2025 21:02:51 +0100 Subject: [PATCH 11/11] use fixed poll delay from const --- source/HomeAssistantApp.mc | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index dcedea8..8677420 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -42,6 +42,8 @@ class HomeAssistantApp extends Application.AppBase { private var mTemplates as Lang.Dictionary = {}; private var mNotifiedNoBle as Lang.Boolean = false; + private const wifiPollDelayMs = 2000; + //! Class Constructor // function initialize() { @@ -458,11 +460,7 @@ class HomeAssistantApp extends Application.AppBase { mNotifiedNoBle = true; setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - - var delay = Settings.getPollDelay(); - if (delay > 0) { - mUpdateTimer.start(method(:startUpdates), delay, false); - } + mUpdateTimer.start(method(:startUpdates), wifiPollDelayMs, false); mUpdating = false; return;