From e9a0c5d137bb4e4c11346b217196c39e2160e3e7 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 26 Aug 2024 18:59:17 +0100 Subject: [PATCH] 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); - } - -}