diff --git a/manifest.xml b/manifest.xml index 6c011ed..3f2a12b 100644 --- a/manifest.xml +++ b/manifest.xml @@ -21,7 +21,7 @@ Use "Monkey C: Edit Application" from the Visual Studio Code command palette to update the application attributes. --> - + + + + + diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml index 1ea3ca6..6482d61 100644 --- a/resources/settings/settings.xml +++ b/resources/settings/settings.xml @@ -42,4 +42,22 @@ type="alphaNumeric" /> + + + + + + + + diff --git a/source/Alert.mc b/source/Alert.mc index ce2511c..dc61cb9 100644 --- a/source/Alert.mc +++ b/source/Alert.mc @@ -30,12 +30,12 @@ using Toybox.Timer; const bRadius = 10; class Alert extends WatchUi.View { - hidden var mTimer; - hidden var mTimeout; - hidden var mText; - hidden var mFont; - hidden var mFgcolor; - hidden var mBgcolor; + private var mTimer; + private var mTimeout; + private var mText; + private var mFont; + private var mFgcolor; + private var mBgcolor; function initialize(params as Lang.Dictionary) { View.initialize(); diff --git a/source/ErrorView.mc b/source/ErrorView.mc index 8cc6d02..3b8318a 100644 --- a/source/ErrorView.mc +++ b/source/ErrorView.mc @@ -26,14 +26,14 @@ using Toybox.WatchUi; using Toybox.Communications; class ErrorView extends ScalableView { - hidden const cSettings as Lang.Dictionary = { + private const cSettings as Lang.Dictionary = { :errorIconMargin => 7f }; // Vertical spacing between the top of the face and the error icon - hidden var mErrorIconMargin; - hidden var mText as Lang.String; - hidden var mErrorIcon; - hidden var mTextArea; + private var mErrorIconMargin; + private var mText as Lang.String; + private var mErrorIcon; + private var mTextArea; function initialize(text as Lang.String) { ScalableView.initialize(); diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 16f4461..33e4649 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -24,20 +24,20 @@ using Toybox.WatchUi; using Toybox.Application.Properties; class HomeAssistantApp extends Application.AppBase { - hidden var mHaMenu; - hidden var strNoApiKey as Lang.String; - hidden var strNoApiUrl as Lang.String; - hidden var strNoConfigUrl as Lang.String; - hidden var strNoPhone as Lang.String; - hidden var strNoInternet as Lang.String; - hidden var strNoResponse as Lang.String; - hidden var strNoMenu as Lang.String; - hidden var strApiFlood as Lang.String; - hidden var strConfigUrlNotFound as Lang.String; - hidden var strUnhandledHttpErr as Lang.String; - hidden var strTrailingSlashErr as Lang.String; - hidden var mItemsToUpdate; // Array initialised by onReturnFetchMenuConfig() - hidden var mNextItemToUpdate = 0; // Index into the above array + private var mHaMenu; + private var strNoApiKey as Lang.String; + private var strNoApiUrl as Lang.String; + private var strNoConfigUrl as Lang.String; + private var strNoPhone as Lang.String; + private var strNoInternet as Lang.String; + private var strNoResponse as Lang.String; + private var strNoMenu as Lang.String; + private var strApiFlood as Lang.String; + private var strConfigUrlNotFound as Lang.String; + private var strUnhandledHttpErr as Lang.String; + private var strTrailingSlashErr as Lang.String; + private var mItemsToUpdate; // Array initialised by onReturnFetchMenuConfig() + private var mNextItemToUpdate = 0; // Index into the above array function initialize() { AppBase.initialize(); @@ -67,22 +67,22 @@ class HomeAssistantApp extends Application.AppBase { if ((Properties.getValue("api_key") as Lang.String).length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): No API key in the application settings."); + System.println("HomeAssistantApp getInitialView(): No API key in the application settings."); } return [new ErrorView(strNoApiKey + "."), new ErrorDelegate()] as Lang.Array; } else if (api_url.length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): No API URL in the application settings."); + System.println("HomeAssistantApp getInitialView(): No API URL in the application settings."); } return [new ErrorView(strNoApiUrl + "."), new ErrorDelegate()] as Lang.Array; } else if (api_url.substring(-1, api_url.length()).equals("/")) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): API URL must not have a trailing slash '/'."); + System.println("HomeAssistantApp getInitialView(): API URL must not have a trailing slash '/'."); } return [new ErrorView(strTrailingSlashErr + "."), new ErrorDelegate()] as Lang.Array; } else if ((Properties.getValue("config_url") as Lang.String).length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): No configuration URL in the application settings."); + System.println("HomeAssistantApp getInitialView(): No configuration URL in the application settings."); } return [new ErrorView(strNoConfigUrl + "."), new ErrorDelegate()] as Lang.Array; } else if (! System.getDeviceSettings().phoneConnected) { diff --git a/source/HomeAssistantIconMenuItem.mc b/source/HomeAssistantIconMenuItem.mc new file mode 100644 index 0000000..f932807 --- /dev/null +++ b/source/HomeAssistantIconMenuItem.mc @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------------- +// +// 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, 31 October 2023 +// +// +// Description: +// +// Menu button that triggers a service. +// +//----------------------------------------------------------------------------------- + +using Toybox.Lang; +using Toybox.WatchUi; +using Toybox.Graphics; +using Toybox.Application.Properties; + +class HomeAssistantIconMenuItem extends WatchUi.IconMenuItem { + private var mHomeAssistantService as HomeAssistantService; + private var mService as Lang.String; + + function initialize( + label as Lang.String or Lang.Symbol, + subLabel as Lang.String or Lang.Symbol or Null, + identifier as Lang.Object or Null, + service as Lang.String or Null, + icon as Graphics.BitmapType or WatchUi.Drawable, + options as { + :alignment as WatchUi.MenuItem.Alignment + } or Null, + haService as HomeAssistantService + ) { + WatchUi.IconMenuItem.initialize( + label, + subLabel, + identifier, + icon, + options + ); + + mHomeAssistantService = haService; + mIdentifier = identifier; + mService = service; + } + + function callService() as Void { + mHomeAssistantService.call(mIdentifier as Lang.String, mService); + } + +} diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc index ddb3d63..a2f914f 100644 --- a/source/HomeAssistantMenuItem.mc +++ b/source/HomeAssistantMenuItem.mc @@ -24,14 +24,8 @@ using Toybox.Graphics; using Toybox.Application.Properties; class HomeAssistantMenuItem extends WatchUi.MenuItem { - hidden var mApiKey as Lang.String; - hidden var strNoPhone as Lang.String; - hidden var strNoInternet as Lang.String; - hidden var strNoResponse as Lang.String; - hidden var strApiFlood as Lang.String; - hidden var strApiUrlNotFound as Lang.String; - hidden var strUnhandledHttpErr as Lang.String; - hidden var mService as Lang.String; + private var mHomeAssistantService as HomeAssistantService; + private var mService as Lang.String; function initialize( label as Lang.String or Lang.Symbol, @@ -41,142 +35,22 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { options as { :alignment as WatchUi.MenuItem.Alignment, :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol - } or Null + } or Null, + haService as HomeAssistantService ) { - strNoPhone = WatchUi.loadResource($.Rez.Strings.NoPhone); - strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); - strNoResponse = WatchUi.loadResource($.Rez.Strings.NoResponse); - strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); - strApiUrlNotFound = WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound); - strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr); - mApiKey = Properties.getValue("api_key"); - mService = service; WatchUi.MenuItem.initialize( label, subLabel, identifier, options ); + + mHomeAssistantService = haService; + mService = service; } - // Callback function after completing the POST request to call a script. - // - function onReturnExecScript(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: " + responseCode); - System.println("HomeAssistantMenuItem onReturnExecScript() Response Data: " + data); - } - if (responseCode == Communications.BLE_HOST_TIMEOUT || responseCode == Communications.BLE_CONNECTION_UNAVAILABLE) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed."); - } - WatchUi.pushView(new ErrorView(strNoPhone + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); - } else if (responseCode == Communications.BLE_QUEUE_FULL) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: BLE_QUEUE_FULL, API calls too rapid."); - } - if (!(WatchUi.getCurrentView()[0] instanceof ErrorView)) { - // Avoid pushing multiple ErrorViews - WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); - } - } else if (responseCode == Communications.NETWORK_REQUEST_TIMED_OUT) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection."); - } - WatchUi.pushView(new ErrorView(strNoResponse), new ErrorDelegate(), WatchUi.SLIDE_UP); - } else if (responseCode == 404) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: 404, page not found. Check API URL setting."); - } - WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); - } else if (responseCode == 200) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript(): Service executed."); - } - var d = data as Lang.Array; - var toast = "Executed"; - for(var i = 0; i < d.size(); i++) { - if ((d[i].get("entity_id") as Lang.String).equals(mIdentifier)) { - toast = (d[i].get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String; - } - } - if (WatchUi has :showToast) { - 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); - } - } else { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript(): Unhandled HTTP response code = " + responseCode); - } - WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); - } - } - - function execScript() as Void { - var options = { - :method => Communications.HTTP_REQUEST_METHOD_POST, - :headers => { - "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, - "Authorization" => "Bearer " + mApiKey - }, - :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON - }; - if (! System.getDeviceSettings().phoneConnected) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): No Phone connection, skipping API call."); - } - WatchUi.pushView(new ErrorView(strNoPhone + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); - } else if (! System.getDeviceSettings().connectionAvailable) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript(): No Internet connection, skipping API call."); - } - WatchUi.pushView(new ErrorView(strNoInternet + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); - } else { - // Updated SDK and got a new error - // ERROR: venu: Cannot find symbol ':substring' on type 'PolyType'. - var id = mIdentifier as Lang.String; - if (mService == null) { - var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + id.substring(0, id.find(".")) + "/" + id.substring(id.find(".")+1, null); - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript() URL=" + url); - System.println("HomeAssistantMenuItem execScript() mIdentifier=" + mIdentifier); - } - Communications.makeWebRequest( - url, - null, - options, - method(:onReturnExecScript) - ); - } else { - var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + mService.substring(0, mService.find(".")) + "/" + mService.substring(mService.find(".")+1, null); - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem execScript() URL=" + url); - System.println("HomeAssistantMenuItem execScript() mService=" + mService); - } - Communications.makeWebRequest( - url, - { - "entity_id" => id - }, - options, - method(:onReturnExecScript) - ); - } - if (Attention has :vibrate) { - Attention.vibrate([ - new Attention.VibeProfile(50, 100), // On for 100ms - new Attention.VibeProfile( 0, 100), // Off for 100ms - new Attention.VibeProfile(50, 100) // On for 100ms - ]); - } - } + function callService() as Void { + mHomeAssistantService.call(mIdentifier as Lang.String, mService); } } diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc new file mode 100644 index 0000000..faa9303 --- /dev/null +++ b/source/HomeAssistantMenuItemFactory.mc @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------------- +// +// 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 & SomeoneOnEarth, 17 November 2023 +// +// +// Description: +// +// MenuItems Factory. +// +//----------------------------------------------------------------------------------- + +using Toybox.Application; +using Toybox.Lang; +using Toybox.WatchUi; + +class HomeAssistantMenuItemFactory { + private var mMenuItemOptions as Lang.Dictionary; + private var mLabelToggle as Lang.Dictionary; + private var strMenuItemTap as Lang.String; + private var bRepresentTypesWithLabels as Lang.Boolean; + private var mTapTypeIcon as WatchUi.Bitmap; + private var mGroupTypeIcon as WatchUi.Bitmap; + private var mHomeAssistantService as HomeAssistantService; + + private static var instance; + + private function initialize() { + mLabelToggle = { + :enabled => WatchUi.loadResource($.Rez.Strings.MenuItemOn) as Lang.String, + :disabled => WatchUi.loadResource($.Rez.Strings.MenuItemOff) as Lang.String + }; + bRepresentTypesWithLabels = Application.Properties.getValue("types_representation") as Lang.Boolean; + + var menuItemAlignment = Application.Properties.getValue("menu_alignment") as Lang.Boolean; + if(menuItemAlignment){ + mMenuItemOptions = { + :alignment => WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT + }; + } else { + mMenuItemOptions = { + :alignment => WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT + }; + } + + strMenuItemTap = WatchUi.loadResource($.Rez.Strings.MenuItemTap); + mTapTypeIcon = new WatchUi.Bitmap({ + :rezId => $.Rez.Drawables.TapTypeIcon, + :locX => WatchUi.LAYOUT_HALIGN_CENTER, + :locY => WatchUi.LAYOUT_VALIGN_CENTER + }); + + mGroupTypeIcon = new WatchUi.Bitmap({ + :rezId => $.Rez.Drawables.GroupTypeIcon, + :locX => WatchUi.LAYOUT_HALIGN_CENTER, + :locY => WatchUi.LAYOUT_VALIGN_CENTER + }); + mHomeAssistantService = new HomeAssistantService(); + } + + static function create() as HomeAssistantMenuItemFactory { + if (instance == null) { + instance = new HomeAssistantMenuItemFactory(); + } + return instance; + } + + function toggle(label as Lang.String or Lang.Symbol, identifier as Lang.Object or Null) as WatchUi.MenuItem { + var subLabel = null; + + if (bRepresentTypesWithLabels == true){ + subLabel=mLabelToggle; + } + + return new HomeAssistantToggleMenuItem( + label, + subLabel, + identifier, + false, + mMenuItemOptions + ); + } + + function tap(label as Lang.String or Lang.Symbol, identifier as Lang.Object or Null, service as Lang.String or Null) as WatchUi.MenuItem { + if (bRepresentTypesWithLabels) { + return new HomeAssistantMenuItem( + label, + strMenuItemTap, + identifier, + service, + mMenuItemOptions, + mHomeAssistantService + ); + } else { + return new HomeAssistantIconMenuItem( + label, + null, + identifier, + service, + mTapTypeIcon, + mMenuItemOptions, + mHomeAssistantService + ); + } + } + + function group(definition as Lang.Dictionary) as WatchUi.MenuItem { + if (bRepresentTypesWithLabels) { + return new HomeAssistantViewMenuItem(definition); + } else { + return new HomeAssistantViewIconMenuItem(definition, mGroupTypeIcon, mMenuItemOptions); + } + } +} diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc new file mode 100644 index 0000000..2f371a9 --- /dev/null +++ b/source/HomeAssistantService.mc @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------------------- +// +// 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 & SomeoneOnEarth, 19 November 2023 +// +// +// Description: +// +// Calling a Home Assistant Service. +// +//----------------------------------------------------------------------------------- + +using Toybox.Lang; +using Toybox.WatchUi; +using Toybox.Graphics; +using Toybox.Application.Properties; + +class HomeAssistantService { + private var mApiKey as Lang.String; + private var strNoPhone as Lang.String; + private var strNoInternet as Lang.String; + private var strNoResponse as Lang.String; + private var strApiFlood as Lang.String; + private var strApiUrlNotFound as Lang.String; + private var strUnhandledHttpErr as Lang.String; + + function initialize() { + strNoPhone = WatchUi.loadResource($.Rez.Strings.NoPhone); + strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); + strNoResponse = WatchUi.loadResource($.Rez.Strings.NoResponse); + strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strApiUrlNotFound = WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound); + strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr); + mApiKey = Properties.getValue("api_key"); + } + + // Callback function after completing the POST request to call a service. + // + function onReturnCall(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, context as Lang.Object) as Void { + var identifier = context as Lang.String; + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall() Response Code: " + responseCode); + System.println("HomeAssistantService onReturnCall() Response Data: " + data); + } + if (responseCode == Communications.BLE_HOST_TIMEOUT || responseCode == Communications.BLE_CONNECTION_UNAVAILABLE) { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed."); + } + WatchUi.pushView(new ErrorView(strNoPhone + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else if (responseCode == Communications.BLE_QUEUE_FULL) { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall() Response Code: BLE_QUEUE_FULL, API calls too rapid."); + } + if (!(WatchUi.getCurrentView()[0] instanceof ErrorView)) { + // Avoid pushing multiple ErrorViews + WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); + } + } else if (responseCode == Communications.NETWORK_REQUEST_TIMED_OUT) { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection."); + } + WatchUi.pushView(new ErrorView(strNoResponse), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else if (responseCode == 404) { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall() Response Code: 404, page not found. Check API URL setting."); + } + WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else if (responseCode == 200) { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall(): Service executed."); + } + var d = data as Lang.Array; + var toast = "Executed"; + for(var i = 0; i < d.size(); i++) { + if ((d[i].get("entity_id") as Lang.String).equals(identifier)) { + toast = (d[i].get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String; + } + } + if (WatchUi has :showToast) { + 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); + } + } else { + if (Globals.scDebug) { + System.println("HomeAssistantService onReturnCall(): Unhandled HTTP response code = " + responseCode); + } + WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); + } + } + + function call(identifier as Lang.String, service as Lang.String) as Void { + var options = { + :method => Communications.HTTP_REQUEST_METHOD_POST, + :headers => { + "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON, + "Authorization" => "Bearer " + mApiKey + }, + :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON, + :context => identifier + }; + if (! System.getDeviceSettings().phoneConnected) { + if (Globals.scDebug) { + System.println("HomeAssistantService call(): No Phone connection, skipping API call."); + } + WatchUi.pushView(new ErrorView(strNoPhone + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else if (! System.getDeviceSettings().connectionAvailable) { + if (Globals.scDebug) { + System.println("HomeAssistantService call(): No Internet connection, skipping API call."); + } + WatchUi.pushView(new ErrorView(strNoInternet + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else { + var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, null); + if (Globals.scDebug) { + System.println("HomeAssistantService call() URL=" + url); + System.println("HomeAssistantService call() service=" + service); + } + Communications.makeWebRequest( + url, + { + "entity_id" => identifier + }, + options, + method(:onReturnCall) + ); + if (Attention has :vibrate) { + Attention.vibrate([ + new Attention.VibeProfile(50, 100), // On for 100ms + new Attention.VibeProfile( 0, 100), // Off for 100ms + new Attention.VibeProfile(50, 100) // On for 100ms + ]); + } + } + } + +} diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 538ae5a..9bbbe89 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -25,23 +25,23 @@ using Toybox.Application.Properties; using Toybox.Timer; class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { - hidden var mApiKey as Lang.String; - hidden var strNoPhone as Lang.String; - hidden var strNoInternet as Lang.String; - hidden var strNoResponse as Lang.String; - hidden var strApiFlood as Lang.String; - hidden var strApiUrlNotFound as Lang.String; - hidden var strUnhandledHttpErr as Lang.String; + private var mApiKey as Lang.String; + private var strNoPhone as Lang.String; + private var strNoInternet as Lang.String; + private var strNoResponse as Lang.String; + private var strApiFlood as Lang.String; + private var strApiUrlNotFound as Lang.String; + private var strUnhandledHttpErr as Lang.String; function initialize( - label as Lang.String or Lang.Symbol, - subLabel as Lang.String or Lang.Symbol or { + label as Lang.String or Lang.Symbol, + subLabel as Lang.String or Lang.Symbol or { :enabled as Lang.String or Lang.Symbol or Null, :disabled as Lang.String or Lang.Symbol or Null } or Null, identifier, - enabled as Lang.Boolean, - options as { + enabled as Lang.Boolean, + options as { :alignment as WatchUi.MenuItem.Alignment, :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol } or Null @@ -131,7 +131,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { if (keepUpdating) { // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. getApp().updateNextMenuItem(); - System.println("HomeAssistantToggleMenuItem onReturnGetState(): Updated " + mIdentifier); } } diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc index fe74847..d8308de 100644 --- a/source/HomeAssistantView.mc +++ b/source/HomeAssistantView.mc @@ -18,15 +18,15 @@ // //----------------------------------------------------------------------------------- +using Toybox.Application; using Toybox.Lang; using Toybox.Graphics; using Toybox.WatchUi; class HomeAssistantView extends WatchUi.Menu2 { - hidden var strMenuItemTap as Lang.String; // List of items that need to have their status updated periodically - hidden var mListToggleItems = []; - hidden var mListMenuItems = []; + private var mListToggleItems = []; + private var mListMenuItems = []; function initialize( definition as Lang.Dictionary, @@ -36,11 +36,6 @@ class HomeAssistantView extends WatchUi.Menu2 { :theme as WatchUi.MenuTheme or Null } or Null ) { - strMenuItemTap = WatchUi.loadResource($.Rez.Strings.MenuItemTap); - var toggle_obj = { - :enabled => WatchUi.loadResource($.Rez.Strings.MenuItemOn) as Lang.String, - :disabled => WatchUi.loadResource($.Rez.Strings.MenuItemOff) as Lang.String - }; if (options == null) { options = { @@ -59,27 +54,13 @@ class HomeAssistantView extends WatchUi.Menu2 { var service = items[i].get("service") as Lang.String or Null; if (type != null && name != null && entity != null) { if (type.equals("toggle")) { - var item = new HomeAssistantToggleMenuItem( - name, - toggle_obj, - entity, - false, - null - ); + var item = HomeAssistantMenuItemFactory.create().toggle(name, entity); addItem(item); mListToggleItems.add(item); } else if (type.equals("tap") && service != null) { - addItem( - new HomeAssistantMenuItem( - name, - strMenuItemTap, - entity, - service, - null - ) - ); + addItem( HomeAssistantMenuItemFactory.create().tap(name, entity, service)); } else if (type.equals("group")) { - var item = new HomeAssistantViewMenuItem(items[i]); + var item = HomeAssistantMenuItemFactory.create().group(items[i]); addItem(item); mListMenuItems.add(item); } @@ -89,10 +70,15 @@ class HomeAssistantView extends WatchUi.Menu2 { function getItemsToUpdate() as Lang.Array { var fullList = []; - var lmi = mListMenuItems as Lang.Array; - for(var i = 0; i < lmi.size(); i++) { - fullList.addAll(lmi[i].getMenuView().getItemsToUpdate()); + + var lmi = mListMenuItems as Lang.Array; + for(var i = 0; i < mListMenuItems.size(); i++) { + var item = lmi[i]; + if (item instanceof HomeAssistantViewMenuItem || item instanceof HomeAssistantViewIconMenuItem) { + fullList.addAll(item.getMenuView().getItemsToUpdate()); + } } + return fullList.addAll(mListToggleItems); } @@ -124,7 +110,13 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { if (Globals.scDebug) { System.println(haItem.getLabel() + " " + haItem.getId()); } - haItem.execScript(); + haItem.callService(); + } else if (item instanceof HomeAssistantIconMenuItem) { + var haItem = item as HomeAssistantIconMenuItem; + if (Globals.scDebug) { + System.println(haItem.getLabel() + " " + haItem.getId()); + } + haItem.callService(); } else if (item instanceof HomeAssistantViewMenuItem) { var haMenuItem = item as HomeAssistantViewMenuItem; if (Globals.scDebug) { @@ -132,6 +124,13 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { } // No delegate state to be amended, so re-use 'self'. WatchUi.pushView(haMenuItem.getMenuView(), self, WatchUi.SLIDE_LEFT); + } else if (item instanceof HomeAssistantViewIconMenuItem) { + var haMenuItem = item as HomeAssistantViewIconMenuItem; + if (Globals.scDebug) { + System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId()); + } + // No delegate state to be amended, so re-use 'self'. + WatchUi.pushView(haMenuItem.getMenuView(), self, WatchUi.SLIDE_LEFT); } else { if (Globals.scDebug) { System.println(item.getLabel() + " " + item.getId()); @@ -143,4 +142,4 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { WatchUi.popView(WatchUi.SLIDE_RIGHT); } -} \ No newline at end of file +} diff --git a/source/HomeAssistantViewIconMenuItem.mc b/source/HomeAssistantViewIconMenuItem.mc new file mode 100644 index 0000000..382444d --- /dev/null +++ b/source/HomeAssistantViewIconMenuItem.mc @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------------- +// +// 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 & SomeoneOnEarth, 16 November 2023 +// +// +// Description: +// +// Menu button with an icon that opens a sub-menu. +// +//----------------------------------------------------------------------------------- + +using Toybox.Lang; +using Toybox.WatchUi; + +class HomeAssistantViewIconMenuItem extends WatchUi.IconMenuItem { + private var mMenu as HomeAssistantView; + + function initialize(definition as Lang.Dictionary, icon as WatchUi.Drawable, options as { + :alignment as WatchUi.MenuItem.Alignment + } or Null) { + var label = definition.get("name") as Lang.String; + var identifier = definition.get("entity") as Lang.String; + + WatchUi.IconMenuItem.initialize( + label, + null, + identifier, + icon, + options + ); + + mMenu = new HomeAssistantView(definition, null); + } + + function getMenuView() as HomeAssistantView { + return mMenu; + } + +} diff --git a/source/HomeAssistantViewMenuItem.mc b/source/HomeAssistantViewMenuItem.mc index 34e3a67..c430b4b 100644 --- a/source/HomeAssistantViewMenuItem.mc +++ b/source/HomeAssistantViewMenuItem.mc @@ -22,7 +22,7 @@ using Toybox.Lang; using Toybox.WatchUi; class HomeAssistantViewMenuItem extends WatchUi.MenuItem { - hidden var mMenu as HomeAssistantView; + private var mMenu as HomeAssistantView; function initialize(definition as Lang.Dictionary) { // definitions.get(...) are Strings here as they have been checked by HomeAssistantView first diff --git a/source/ScalableView.mc b/source/ScalableView.mc index 7744d7a..febf84d 100644 --- a/source/ScalableView.mc +++ b/source/ScalableView.mc @@ -23,7 +23,7 @@ using Toybox.WatchUi; using Toybox.Math; class ScalableView extends WatchUi.View { - hidden var mScreenWidth; + private var mScreenWidth; function initialize() { View.initialize();