From 685cda7924d912c68e3dc93d9e906ae0dfd13917 Mon Sep 17 00:00:00 2001 From: Philip Abbey Date: Sun, 25 Aug 2024 19:34:29 +0100 Subject: [PATCH] Added template option to toggle menu items --- source/HomeAssistantMenuItemFactory.mc | 2 + source/HomeAssistantToggleMenuItem.mc | 152 +++++++++++++++++++++++-- source/HomeAssistantView.mc | 2 +- 3 files changed, 145 insertions(+), 11 deletions(-) diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc index a9be246..d19cd34 100644 --- a/source/HomeAssistantMenuItemFactory.mc +++ b/source/HomeAssistantMenuItemFactory.mc @@ -67,10 +67,12 @@ class HomeAssistantMenuItemFactory { function toggle( label as Lang.String or Lang.Symbol, entity_id as Lang.String or Null, + template as Lang.String or Null, confirm as Lang.Boolean ) as WatchUi.MenuItem { return new HomeAssistantToggleMenuItem( label, + template, confirm, { "entity_id" => entity_id }, mMenuItemOptions diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 3a49d2f..ac6d8c9 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -25,21 +25,24 @@ using Toybox.Application.Properties; using Toybox.Timer; class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { - private var mConfirm as Lang.Boolean; - private var mData as Lang.Dictionary; + private var mConfirm as Lang.Boolean; + private var mData as Lang.Dictionary; + private var mTemplate as Lang.String; function initialize( - label as Lang.String or Lang.Symbol, - confirm as Lang.Boolean, - data as Lang.Dictionary or Null, - options as { + label as Lang.String or Lang.Symbol, + template as Lang.String, + confirm as Lang.Boolean, + data as Lang.Dictionary or Null, + options as { :alignment as WatchUi.MenuItem.Alignment, :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol } or Null ) { WatchUi.ToggleMenuItem.initialize(label, null, null, false, options); - mConfirm = confirm; - mData = data; + mConfirm = confirm; + mData = data; + mTemplate = template; } private function setUiToggle(state as Null or Lang.String) as Void { @@ -88,6 +91,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { 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 @@ -123,8 +127,13 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String); } setUiToggle(state); - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().updateNextMenuItem(); + 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: @@ -272,4 +281,127 @@ 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/HomeAssistantView.mc b/source/HomeAssistantView.mc index 6a82046..1bb431e 100644 --- a/source/HomeAssistantView.mc +++ b/source/HomeAssistantView.mc @@ -62,7 +62,7 @@ class HomeAssistantView extends WatchUi.Menu2 { } if (type != null && name != null) { if (type.equals("toggle") && entity != null) { - addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, confirm)); + addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm)); } else if (type.equals("template") && content != null) { if (service == null) { addItem(HomeAssistantMenuItemFactory.create().template_notap(name, content));