From e9a0c5d137bb4e4c11346b217196c39e2160e3e7 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 26 Aug 2024 18:59:17 +0100 Subject: [PATCH 1/6] Single request to update --- source/ErrorView.mc | 4 +- source/HomeAssistantApp.mc | 146 ++++++++++---- source/HomeAssistantGroupMenuItem.mc | 24 ++- source/HomeAssistantTapMenuItem.mc | 7 + source/HomeAssistantTemplateMenuItem.mc | 20 +- source/HomeAssistantToggleMenuItem.mc | 246 ++---------------------- source/TemplateMenuItem.mc | 182 ------------------ 7 files changed, 167 insertions(+), 462 deletions(-) delete mode 100644 source/TemplateMenuItem.mc diff --git a/source/ErrorView.mc b/source/ErrorView.mc index f4a99da..b696381 100644 --- a/source/ErrorView.mc +++ b/source/ErrorView.mc @@ -116,10 +116,10 @@ class ErrorView extends ScalableView { static function unShow() as Void { if (mShown) { WatchUi.popView(WatchUi.SLIDE_DOWN); - // The call to 'updateNextMenuItem()' must be on another thread so that the view is popped above. + // The call to 'updateMenuItems()' must be on another thread so that the view is popped above. var myTimer = new Timer.Timer(); // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiResume, false); + myTimer.start(getApp().method(:updateMenuItems), Globals.scApiResume, false); // This must be last to avoid a race condition with show(), where the // ErrorView can't be dismissed. mShown = false; diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 18d17e3..674933a 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -35,10 +35,8 @@ class HomeAssistantApp extends Application.AppBase { private var mUpdateTimer as Timer.Timer or Null; // Array initialised by onReturnFetchMenuConfig() private var mItemsToUpdate as Lang.Array or Null; - private var mNextItemToUpdate as Lang.Number = 0; // Index into the above array private var mIsGlance as Lang.Boolean = false; private var mIsApp as Lang.Boolean = false; // Or Widget - private var mIsInitUpdateCompl as Lang.Boolean = false; private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates function initialize() { @@ -262,15 +260,118 @@ class HomeAssistantApp extends Application.AppBase { mQuitTimer.begin(); } + var mTemplates as Lang.Dictionary = {}; function startUpdates() { if (mHaMenu != null and !mUpdating) { mItemsToUpdate = mHaMenu.getItemsToUpdate(); // Start the continuous update process that continues for as long as the application is running. - // The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion. - if (mItemsToUpdate.size() > 0) { - mUpdating = true; - updateNextMenuItemInternal(); + mTemplates = {}; + for (var i = 0; i < mItemsToUpdate.size(); i++) { + var item = mItemsToUpdate[i]; + mTemplates.put(i.toString(), { + "template" => item.buildTemplate() + }); } + updateMenuItems(); + } + } + + 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(); + // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. + myTimer.start(method(:updateMenuItems), Globals.scApiBackoff, 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; + for (var i = 0; i < mItemsToUpdate.size(); i++) { + var item = mItemsToUpdate[i]; + var state = data.get(i.toString()); + item.updateState(state); + } + 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); + } + + function updateMenuItems() as Void { + if (! System.getDeviceSettings().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) { + // 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 { + // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates + var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(); + // System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'"); + Communications.makeWebRequest( + url, + { + "type" => "render_template", + "data" => mTemplates + }, + { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON + }, + method(:onReturnUpdateMenuItems) + ); } } @@ -403,45 +504,14 @@ class HomeAssistantApp extends Application.AppBase { WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE); } - function updateNextMenuItem() as Void { - var delay = Settings.getPollDelay(); - if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) { - mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false); - } else { - updateNextMenuItemInternal(); - } - } - // Only call this function 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(); - mIsInitUpdateCompl = false; - // Start from the beginning, or we will only get a partial round of updates before mIsInitUpdateCompl is flipped. - mNextItemToUpdate = 0; // For immediate updates - updateNextMenuItem(); - } - } - - // We need to spread out the API calls so as not to overload the results queue and cause Communications.BLE_QUEUE_FULL - // (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms. - function updateNextMenuItemInternal() as Void { - if (mItemsToUpdate != null) { - // System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl); - mItemsToUpdate[mNextItemToUpdate].getState(); - // mNextItemToUpdate = (mNextItemToUpdate + 1) % mItemsToUpdate.size() - But with roll-over detection - if (mNextItemToUpdate == mItemsToUpdate.size()-1) { - // Last item completed return to the start of the list - mNextItemToUpdate = 0; - mIsInitUpdateCompl = true; - } else { - mNextItemToUpdate++; - } - // } else { - // System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update"); + updateMenuItems(); } } diff --git a/source/HomeAssistantGroupMenuItem.mc b/source/HomeAssistantGroupMenuItem.mc index 2239f19..2ba7152 100644 --- a/source/HomeAssistantGroupMenuItem.mc +++ b/source/HomeAssistantGroupMenuItem.mc @@ -22,7 +22,8 @@ using Toybox.Lang; using Toybox.WatchUi; -class HomeAssistantGroupMenuItem extends TemplateMenuItem { +class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem { + private var mTemplate as Lang.String or Null; private var mMenu as HomeAssistantView; function initialize( @@ -34,20 +35,33 @@ class HomeAssistantGroupMenuItem extends TemplateMenuItem { } or Null ) { - TemplateMenuItem.initialize( + WatchUi.IconMenuItem.initialize( definition.get("name") as Lang.String, - template, - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().method(:updateNextMenuItem), + null, + null, icon, options ); + mTemplate = template; mMenu = new HomeAssistantView(definition, null); } + function buildTemplate() as Lang.String or Null { + return mTemplate; + } + + function updateState(data as Lang.String or Null) as Void { + setSubLabel(data); + WatchUi.requestUpdate(); + } + function getMenuView() as HomeAssistantView { return mMenu; } + function hasTemplate() as Lang.Boolean { + return mTemplate != null; + } + } diff --git a/source/HomeAssistantTapMenuItem.mc b/source/HomeAssistantTapMenuItem.mc index 9623a63..5fee882 100644 --- a/source/HomeAssistantTapMenuItem.mc +++ b/source/HomeAssistantTapMenuItem.mc @@ -53,6 +53,13 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem { mData = data; } + function buildTemplate() as Lang.String or Null { + return null; + } + + function updateState(data as Lang.String or Null) as Void { + } + function callService() as Void { if (mConfirm) { WatchUi.pushView( diff --git a/source/HomeAssistantTemplateMenuItem.mc b/source/HomeAssistantTemplateMenuItem.mc index fd8e13a..78adce1 100644 --- a/source/HomeAssistantTemplateMenuItem.mc +++ b/source/HomeAssistantTemplateMenuItem.mc @@ -26,8 +26,9 @@ using Toybox.Lang; using Toybox.WatchUi; using Toybox.Graphics; -class HomeAssistantTemplateMenuItem extends TemplateMenuItem { +class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem { private var mHomeAssistantService as HomeAssistantService; + private var mTemplate as Lang.String; private var mService as Lang.String or Null; private var mConfirm as Lang.Boolean; private var mData as Lang.Dictionary or Null; @@ -44,21 +45,30 @@ class HomeAssistantTemplateMenuItem extends TemplateMenuItem { } or Null, haService as HomeAssistantService ) { - TemplateMenuItem.initialize( + WatchUi.IconMenuItem.initialize( label, - template, - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().method(:updateNextMenuItem), + null, + null, icon, options ); mHomeAssistantService = haService; + mTemplate = template; mService = service; mConfirm = confirm; mData = data; } + function buildTemplate() as Lang.String or Null { + return mTemplate; + } + + function updateState(data as Lang.String or Null) as Void { + setSubLabel(data); + WatchUi.requestUpdate(); + } + function callService() as Void { if (mConfirm) { WatchUi.pushView( diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index ac6d8c9..e3687ea 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -57,117 +57,26 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } } - // Callback function after completing the GET request to fetch the status. - // Terminate updating the toggle menu items via the chain of calls for a permanent network - // error. The ErrorView cancellation will resume the call chain. - // - function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { - // System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode); - // System.println("HomeAssistantToggleMenuItem onReturnGetState() 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("HomeAssistantToggleMenuItem onReturnGetState() 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("HomeAssistantToggleMenuItem onReturnGetState() 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("HomeAssistantToggleMenuItem onReturnGetState() 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("HomeAssistantToggleMenuItem onReturnGetState() 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("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?"); - var myTimer = new Timer.Timer(); - // Abandon the update to this menu item, and any template, and move on to the next with a back-off delay. - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false); - // Revert status - status = getApp().getApiStatus(); - break; - - case 404: - var msg = null; - if (data != null) { - msg = data.get("message"); - } - if (msg != null) { - // Should be an HTTP 404 according to curl queries - // System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404. " + mData.get("entity_id") + " " + msg); - ErrorView.show("HTTP 404, " + mData.get("entity_id") + ". " + data.get("message")); - } else { - // System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String); - } - break; - - case 405: - // System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 405. " + mData.get("entity_id") + " " + data.get("message")); - ErrorView.show("HTTP 405, " + mData.get("entity_id") + ". " + data.get("message")); - - break; - - case 200: - status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String; - var state = data.get("state") as Lang.String; - // System.println((data.get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); - if (getLabel().equals("...")) { - setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String); - } - setUiToggle(state); - if (mTemplate == null) { - // Nothing more to do - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().updateNextMenuItem(); - } else { - updateTemplate(); - } - break; - - default: - // System.println("HomeAssistantToggleMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode); + function buildTemplate() as Lang.String or Null { + if (mTemplate == null) { + return "{{states('" + mData.get("entity_id") + "')}}"; } - getApp().setApiStatus(status); + return "{{states('" + mData.get("entity_id") + "')}}," + mTemplate; } - function getState() as Void { - if (! System.getDeviceSettings().phoneConnected) { - // System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { - // System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else { - var url = Settings.getApiUrl() + "/states/" + mData.get("entity_id"); - // System.println("HomeAssistantToggleMenuItem getState() URL=" + url); - Communications.makeWebRequest( - url, - null, - { - :method => Communications.HTTP_REQUEST_METHOD_GET, - :headers => { - "Authorization" => "Bearer " + Settings.getApiKey() - }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON - }, - method(:onReturnGetState) - ); + function updateState(data as Lang.String or Null) as Void { + if (data == null) { + setSubLabel(null); + WatchUi.requestUpdate(); + return; } + if (mTemplate == null) { + setUiToggle(data); + return; + } + var split = data.find(","); + setSubLabel(data.substring(split + 1, data.length())); + setUiToggle(data.substring(0, split)); } // Callback function after completing the POST request to set the status. @@ -281,127 +190,4 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { setState(b); } - // Callback function after completing the GET request to fetch the status. - // Terminate updating the toggle menu items via the chain of calls for a permanent network - // error. The ErrorView cancellation will resume the call chain. - // - function onReturnUpdateTemplate(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void { - // System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode); - // System.println("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?"); - var myTimer = new Timer.Timer(); - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false); - // Revert status - status = getApp().getApiStatus(); - break; - - case 404: - // System.println("HomeAssistantTemplateMenuItem onReturnGetState() 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("HomeAssistantTemplateMenuItem onReturnGetState() 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; - var label = data.get("request"); - if (label == null) { - setSubLabel($.Rez.Strings.Empty); - } else if(label instanceof Lang.String) { - setSubLabel(label); - } else if(label instanceof Lang.Dictionary) { - // System.println("HomeAssistantTemplateMenuItem onReturnGetState() label = " + label); - if (label.get("error") != null) { - setSubLabel($.Rez.Strings.TemplateError); - } else { - setSubLabel($.Rez.Strings.PotentialError); - } - } else { - // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. - setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); - } - requestUpdate(); - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().updateNextMenuItem(); - break; - - default: - // System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode); - } - getApp().setApiStatus(status); - } - - // Massive code duplication from TemplateMenuItem, but cannot inherit from two classes. - // - function updateTemplate() as Void { - if (mTemplate == null) { - // Nothing to do here. - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().updateNextMenuItem(); - } else { - if (! System.getDeviceSettings().phoneConnected) { - // System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { - // System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else { - // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates - var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(); - // System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'"); - Communications.makeWebRequest( - url, - { - "type" => "render_template", - "data" => { - "request" => { - "template" => mTemplate - } - } - }, - { - :method => Communications.HTTP_REQUEST_METHOD_POST, - :headers => { - "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON - }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON - }, - method(:onReturnUpdateTemplate) - ); - } - } - } - } diff --git a/source/TemplateMenuItem.mc b/source/TemplateMenuItem.mc deleted file mode 100644 index 89e0431..0000000 --- a/source/TemplateMenuItem.mc +++ /dev/null @@ -1,182 +0,0 @@ -//----------------------------------------------------------------------------------- -// -// 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, 24 August 2024 -// -// -// Description: -// -// Menu button that renders a Home Assistant Template. -// -// Reference: -// * https://developers.home-assistant.io/docs/api/rest/ -// * https://www.home-assistant.io/docs/configuration/templating -// -//----------------------------------------------------------------------------------- - -using Toybox.Lang; -using Toybox.WatchUi; -using Toybox.Graphics; - -class TemplateMenuItem extends WatchUi.IconMenuItem { - private var mTemplate as Lang.String; - private var mCallback as Method() as Void; - - function initialize( - label as Lang.String or Lang.Symbol, - template as Lang.String, - // Do not use Lang.Method as it does not compile! - callback as Method() as Void, - icon as Graphics.BitmapType or WatchUi.Drawable, - options as { - :alignment as WatchUi.MenuItem.Alignment - } or Null - ) { - WatchUi.IconMenuItem.initialize( - label, - null, - null, - icon, - options - ); - - mTemplate = template; - mCallback = callback; - } - - // Callback function after completing the GET request to fetch the status. - // Terminate updating the toggle menu items via the chain of calls for a permanent network - // error. The ErrorView cancellation will resume the call chain. - // - function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void { - // System.println("TemplateMenuItem onReturnGetState() Response Code: " + responseCode); - // System.println("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?"); - var myTimer = new Timer.Timer(); - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false); - // Revert status - status = getApp().getApiStatus(); - break; - - case 404: - // System.println("TemplateMenuItem onReturnGetState() 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("TemplateMenuItem onReturnGetState() 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; - var label = data.get("request"); - if (label == null) { - setSubLabel($.Rez.Strings.Empty); - } else if(label instanceof Lang.String) { - setSubLabel(label); - } else if(label instanceof Lang.Dictionary) { - // System.println("TemplateMenuItem onReturnGetState() label = " + label); - if (label.get("error") != null) { - setSubLabel($.Rez.Strings.TemplateError); - } else { - setSubLabel($.Rez.Strings.PotentialError); - } - } else { - // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. - setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); - } - requestUpdate(); - if (mCallback != null) { - mCallback.invoke(); - } - break; - - default: - // System.println("TemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode); - } - getApp().setApiStatus(status); - } - - function getState() as Void { - if (mTemplate == null) { - // Nothing to do here. - if (mCallback != null) { - mCallback.invoke(); - } - } else { - if (! System.getDeviceSettings().phoneConnected) { - // System.println("TemplateMenuItem getState(): No Phone connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else if (! System.getDeviceSettings().connectionAvailable) { - // System.println("TemplateMenuItem getState(): No Internet connection, skipping API call."); - ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + "."); - getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); - } else { - // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates - var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(); - // System.println("TemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'"); - Communications.makeWebRequest( - url, - { - "type" => "render_template", - "data" => { - "request" => { - "template" => mTemplate - } - } - }, - { - :method => Communications.HTTP_REQUEST_METHOD_POST, - :headers => { - "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON - }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON - }, - method(:onReturnGetState) - ); - } - } - } - - function hasTemplate() as Lang.Boolean { - return (mTemplate != null); - } - -} From 2a48790f9c3038db4681e534eae2452c40f20e26 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 26 Aug 2024 20:09:24 +0100 Subject: [PATCH 2/6] Individual errors --- compile_sim.cmd | 4 +-- source/HomeAssistantGroupMenuItem.mc | 18 +++++++++++-- source/HomeAssistantTemplateMenuItem.mc | 18 +++++++++++-- source/HomeAssistantToggleMenuItem.mc | 35 ++++++++++++++++--------- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/compile_sim.cmd b/compile_sim.cmd index 1005efb..5708341 100644 --- a/compile_sim.cmd +++ b/compile_sim.cmd @@ -21,7 +21,7 @@ rem rem ----------------------------------------------------------------------------------- rem Check this path is correct for your Java installation -set JAVA_PATH=C:\Program Files\Java\jdk-22\bin +set JAVA_PATH=C:\Program Files\Java\jdk-21\bin\ rem SDK_PATH should work for all users set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg" set SDK_PATH=%SDK_PATH:~0,-1%\bin @@ -100,7 +100,7 @@ rem Compile PRG for a single device for side loading -jar %SDK_PATH%\monkeybrains.jar ^ --output %SRC%\bin\HomeAssistant.prg ^ --jungles %SRC%\%JUNGLE% ^ - --private-key %SRC%\..\developer_key ^ + --private-key "C:\Users\josep\AppData\Roaming\Garmin\ConnectIQ\Key\developer_key" ^ --device %DEVICE%_sim ^ --warn ^ --release diff --git a/source/HomeAssistantGroupMenuItem.mc b/source/HomeAssistantGroupMenuItem.mc index 2ba7152..7d44f4d 100644 --- a/source/HomeAssistantGroupMenuItem.mc +++ b/source/HomeAssistantGroupMenuItem.mc @@ -51,8 +51,22 @@ class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem { return mTemplate; } - function updateState(data as Lang.String or Null) as Void { - setSubLabel(data); + function updateState(data as Lang.String or Lang.Dictionary or Null) as Void { + if (data == null) { + setSubLabel($.Rez.Strings.Empty); + } else if(data instanceof Lang.String) { + setSubLabel(data); + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAsistantGroupMenuItem updateState() data = " + data); + if (data.get("error") != null) { + setSubLabel($.Rez.Strings.TemplateError); + } else { + setSubLabel($.Rez.Strings.PotentialError); + } + } else { + // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. + setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); + } WatchUi.requestUpdate(); } diff --git a/source/HomeAssistantTemplateMenuItem.mc b/source/HomeAssistantTemplateMenuItem.mc index 78adce1..adfa0d8 100644 --- a/source/HomeAssistantTemplateMenuItem.mc +++ b/source/HomeAssistantTemplateMenuItem.mc @@ -64,8 +64,22 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem { return mTemplate; } - function updateState(data as Lang.String or Null) as Void { - setSubLabel(data); + function updateState(data as Lang.String or Lang.Dictionary or Null) as Void { + if (data == null) { + setSubLabel($.Rez.Strings.Empty); + } else if(data instanceof Lang.String) { + setSubLabel(data); + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAsistantTemplateMenuItem updateState() data = " + data); + if (data.get("error") != null) { + setSubLabel($.Rez.Strings.TemplateError); + } else { + setSubLabel($.Rez.Strings.PotentialError); + } + } else { + // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. + setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); + } WatchUi.requestUpdate(); } diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index e3687ea..07e7b64 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -49,10 +49,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { if (state != null) { if (state.equals("on") && !isEnabled()) { setEnabled(true); - WatchUi.requestUpdate(); } else if (state.equals("off") && isEnabled()) { setEnabled(false); - WatchUi.requestUpdate(); } } } @@ -64,19 +62,29 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { return "{{states('" + mData.get("entity_id") + "')}}," + mTemplate; } - function updateState(data as Lang.String or Null) as Void { + function updateState(data as Lang.String or Lang.Dictionary or Null) as Void { if (data == null) { - setSubLabel(null); - WatchUi.requestUpdate(); - return; + setSubLabel($.Rez.Strings.Empty); + } else if(data instanceof Lang.String) { + if (mTemplate == null) { + setUiToggle(data); + } else { + var split = data.find(","); + setSubLabel(data.substring(split + 1, data.length())); + setUiToggle(data.substring(0, split)); + } + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAsistantToggleMenuItem updateState() data = " + data); + if (data.get("error") != null) { + setSubLabel($.Rez.Strings.TemplateError); + } else { + setSubLabel($.Rez.Strings.PotentialError); + } + } else { + // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. + setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); } - if (mTemplate == null) { - setUiToggle(data); - return; - } - var split = data.find(","); - setSubLabel(data.substring(split + 1, data.length())); - setUiToggle(data.substring(0, split)); + WatchUi.requestUpdate(); } // Callback function after completing the POST request to set the status. @@ -123,6 +131,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { 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(); } } status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String; From bd37d5f2a8947e1bef9aac67c35273401523c70e Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 26 Aug 2024 20:11:19 +0100 Subject: [PATCH 3/6] Allow toggles to work if template fails --- source/HomeAssistantApp.mc | 17 +++++++++-- source/HomeAssistantGroupMenuItem.mc | 2 +- source/HomeAssistantToggleMenuItem.mc | 43 +++++++++++++++++++-------- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 674933a..1755d00 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -268,9 +268,17 @@ class HomeAssistantApp extends Application.AppBase { mTemplates = {}; for (var i = 0; i < mItemsToUpdate.size(); i++) { var item = mItemsToUpdate[i]; - mTemplates.put(i.toString(), { - "template" => item.buildTemplate() - }); + var template = item.buildTemplate(); + if (template != null) { + mTemplates.put(i.toString(), { + "template" => template + }); + } + if (item instanceof HomeAssistantToggleMenuItem) { + mTemplates.put(i.toString() + "t", { + "template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate() + }); + } } updateMenuItems(); } @@ -328,6 +336,9 @@ class HomeAssistantApp extends Application.AppBase { var item = mItemsToUpdate[i]; var state = data.get(i.toString()); item.updateState(state); + if (item instanceof HomeAssistantToggleMenuItem) { + (item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t")); + } } var delay = Settings.getPollDelay(); if (delay > 0) { diff --git a/source/HomeAssistantGroupMenuItem.mc b/source/HomeAssistantGroupMenuItem.mc index 7d44f4d..e53ea9c 100644 --- a/source/HomeAssistantGroupMenuItem.mc +++ b/source/HomeAssistantGroupMenuItem.mc @@ -53,7 +53,7 @@ class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem { function updateState(data as Lang.String or Lang.Dictionary or Null) as Void { if (data == null) { - setSubLabel($.Rez.Strings.Empty); + setSubLabel(null); } else if(data instanceof Lang.String) { setSubLabel(data); } else if(data instanceof Lang.Dictionary) { diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 07e7b64..caee2c0 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -56,23 +56,17 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } function buildTemplate() as Lang.String or Null { - if (mTemplate == null) { - return "{{states('" + mData.get("entity_id") + "')}}"; - } - return "{{states('" + mData.get("entity_id") + "')}}," + mTemplate; + return mTemplate; + } + function buildToggleTemplate() as Lang.String or Null { + return "{{states('" + mData.get("entity_id") + "')}}"; } function updateState(data as Lang.String or Lang.Dictionary or Null) as Void { if (data == null) { - setSubLabel($.Rez.Strings.Empty); + setSubLabel(null); } else if(data instanceof Lang.String) { - if (mTemplate == null) { - setUiToggle(data); - } else { - var split = data.find(","); - setSubLabel(data.substring(split + 1, data.length())); - setUiToggle(data.substring(0, split)); - } + setSubLabel(data); } else if(data instanceof Lang.Dictionary) { // System.println("HomeAsistantToggleMenuItem updateState() data = " + data); if (data.get("error") != null) { @@ -86,6 +80,31 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } WatchUi.requestUpdate(); } + function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void { + if (data == null) { + setUiToggle("off"); + } else if(data instanceof Lang.String) { + setUiToggle(data); + if (mTemplate == null and data.equals("unavailable")) { + setSubLabel($.Rez.Strings.Unavailable); + } + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAsistantToggleMenuItem updateState() data = " + data); + if (mTemplate == null) { + if (data.get("error") != null) { + setSubLabel($.Rez.Strings.TemplateError); + } else { + setSubLabel($.Rez.Strings.PotentialError); + } + } + } else { + // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error. + if (mTemplate == null) { + setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); + } + } + WatchUi.requestUpdate(); + } // Callback function after completing the POST request to set the status. // From b476da66679f49b13acf8e133b58357e8ad1a30b Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Wed, 28 Aug 2024 08:45:07 +0100 Subject: [PATCH 4/6] Revert compile_sim.cmd Signed-off-by: Joseph Abbey --- compile_sim.cmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compile_sim.cmd b/compile_sim.cmd index 5708341..1005efb 100644 --- a/compile_sim.cmd +++ b/compile_sim.cmd @@ -21,7 +21,7 @@ rem rem ----------------------------------------------------------------------------------- rem Check this path is correct for your Java installation -set JAVA_PATH=C:\Program Files\Java\jdk-21\bin\ +set JAVA_PATH=C:\Program Files\Java\jdk-22\bin rem SDK_PATH should work for all users set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg" set SDK_PATH=%SDK_PATH:~0,-1%\bin @@ -100,7 +100,7 @@ rem Compile PRG for a single device for side loading -jar %SDK_PATH%\monkeybrains.jar ^ --output %SRC%\bin\HomeAssistant.prg ^ --jungles %SRC%\%JUNGLE% ^ - --private-key "C:\Users\josep\AppData\Roaming\Garmin\ConnectIQ\Key\developer_key" ^ + --private-key %SRC%\..\developer_key ^ --device %DEVICE%_sim ^ --warn ^ --release From 47a8a6e4e6758a1b916fc32f0f4f001f4f855564 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Wed, 28 Aug 2024 08:52:41 +0100 Subject: [PATCH 5/6] New poll delay property id --- resources/settings/properties.xml | 2 +- resources/settings/settings.xml | 2 +- source/Settings.mc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml index 37f156a..06b7fbb 100644 --- a/resources/settings/properties.xml +++ b/resources/settings/properties.xml @@ -56,7 +56,7 @@ this delayfor an "always open" mode of operation, which then drains the watch battery from the additional API access activity. --> - 0 + 5