mirror of
				https://github.com/house-of-abbey/GarminHomeAssistant.git
				synced 2025-10-31 07:48:13 +00:00 
			
		
		
		
	1. attribute is option, so needs a different template in the API call when absent. 2. Automatically derive the format string from the picker step value for any precision of step. 3. Changed all Lang.String representations of numbers to Lang.Number or Lang.Float. I'm keen to remove the use of strings to hold a numeric value. 4. Tidied up and completed some code comments. 5. Adjusted the JSON schema definition. This is still not finished as the 'picker' object is required for 'numeric' menu items and must not be present for the others. Additional schema changes are required for greater precision. 6. Moved fields over from 'data' to 'picker'.
		
			
				
	
	
		
			296 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			MonkeyC
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			MonkeyC
		
	
	
	
	
	
| //-----------------------------------------------------------------------------------
 | |
| //
 | |
| // 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 & Someone0nEarth & moesterheld, 31 October 2023
 | |
| //
 | |
| //-----------------------------------------------------------------------------------
 | |
| 
 | |
| using Toybox.Application;
 | |
| using Toybox.Lang;
 | |
| using Toybox.Graphics;
 | |
| using Toybox.System;
 | |
| using Toybox.WatchUi;
 | |
| 
 | |
| //! Home Assistant menu construction.
 | |
| //
 | |
| class HomeAssistantView extends WatchUi.Menu2 {
 | |
| 
 | |
|     //! Class Constructor
 | |
|     //
 | |
|     function initialize(
 | |
|         definition as Lang.Dictionary,
 | |
|         options as {
 | |
|             :focus as Lang.Number,
 | |
|             :icon  as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
 | |
|         }?
 | |
|     ) {
 | |
|         if (options == null) {
 | |
|             options = { :title => definition.get("title") as Lang.String };
 | |
|         } else {
 | |
|             options.put(:title, definition.get("title") as Lang.String);
 | |
|         }
 | |
|         WatchUi.Menu2.initialize(options);
 | |
| 
 | |
|         var items = definition.get("items") as Lang.Array<Lang.Dictionary>;
 | |
|         for (var i = 0; i < items.size(); i++) {
 | |
|             if (items[i] instanceof(Lang.Dictionary)) {
 | |
|                 var type       = items[i].get("type")       as Lang.String?;
 | |
|                 var name       = items[i].get("name")       as Lang.String?;
 | |
|                 var content    = items[i].get("content")    as Lang.String?;
 | |
|                 var entity     = items[i].get("entity")     as Lang.String?;
 | |
|                 var tap_action = items[i].get("tap_action") as Lang.Dictionary?;
 | |
|                 var service    = items[i].get("service")    as Lang.String?; // Deprecated schema
 | |
|                 var confirm    = false                      as Lang.Boolean?;
 | |
|                 var pin        = false                      as Lang.Boolean?;
 | |
|                 var data       = null                       as Lang.Dictionary?;
 | |
|                 var enabled    = true                       as Lang.Boolean?;
 | |
|                 var exit       = false                      as Lang.Boolean?;
 | |
|                 if (items[i].get("enabled") != null) {
 | |
|                     enabled = items[i].get("enabled");       // Optional
 | |
|                 }
 | |
|                 if (items[i].get("exit") != null) {
 | |
|                     exit = items[i].get("exit");             // Optional
 | |
|                 }
 | |
|                 if (tap_action != null) {
 | |
|                     service = tap_action.get("service");
 | |
|                     data    = tap_action.get("data");        // Optional
 | |
|                     if (tap_action.get("confirm") != null) {
 | |
|                         confirm = tap_action.get("confirm"); // Optional
 | |
|                     }
 | |
|                     if (tap_action.get("pin") != null) {
 | |
|                         pin = tap_action.get("pin");         // Optional
 | |
|                     }
 | |
|                 }
 | |
|                 if (type != null && name != null && enabled) {
 | |
|                     if (type.equals("toggle") && entity != null) {
 | |
|                         addItem(HomeAssistantMenuItemFactory.create().toggle(
 | |
|                             name,
 | |
|                             entity,
 | |
|                             content,
 | |
|                             {
 | |
|                                 :exit    => exit,
 | |
|                                 :confirm => confirm,
 | |
|                                 :pin     => pin
 | |
|                             }
 | |
|                         ));
 | |
|                     } else if (type.equals("tap") && service != null) {
 | |
|                         addItem(HomeAssistantMenuItemFactory.create().tap(
 | |
|                             name,
 | |
|                             entity,
 | |
|                             content,
 | |
|                             service,
 | |
|                             data,
 | |
|                             {
 | |
|                                 :exit    => exit,
 | |
|                                 :confirm => confirm,
 | |
|                                 :pin     => pin
 | |
|                             }
 | |
|                         ));
 | |
|                     } else if (type.equals("template") && content != null) {
 | |
|                         // NB. "template" is deprecated in the schema and remains only for backward compatibility. All menu items can now use templates, so the replacement is "info".
 | |
|                         // The exit option is dependent on the type of template.
 | |
|                         if (tap_action == null) {
 | |
|                             // No exit from an information only item
 | |
|                             addItem(HomeAssistantMenuItemFactory.create().tap(
 | |
|                                 name,
 | |
|                                 entity,
 | |
|                                 content,
 | |
|                                 service,
 | |
|                                 data,
 | |
|                                 {
 | |
|                                     :exit    => false,
 | |
|                                     :confirm => confirm,
 | |
|                                     :pin     => pin
 | |
|                                 }
 | |
|                             ));
 | |
|                         } else {
 | |
|                             // You may exit from template item with a 'tap_action'.
 | |
|                             addItem(HomeAssistantMenuItemFactory.create().tap(
 | |
|                                 name,
 | |
|                                 entity,
 | |
|                                 content,
 | |
|                                 service,
 | |
|                                 data,
 | |
|                                 {
 | |
|                                     :exit    => exit,
 | |
|                                     :confirm => confirm,
 | |
|                                     :pin     => pin
 | |
|                                 }
 | |
|                             ));
 | |
|                         }
 | |
|                     } else if (type.equals("numeric") && service != null) {
 | |
|                         if (tap_action != null) {
 | |
|                             var picker = tap_action.get("picker") as Lang.Dictionary?;
 | |
|                             if (picker != null) {
 | |
|                                 addItem(HomeAssistantMenuItemFactory.create().numeric(
 | |
|                                     name,
 | |
|                                     entity,
 | |
|                                     content,
 | |
|                                     service,
 | |
|                                     picker,
 | |
|                                     {
 | |
|                                         :exit    => exit,
 | |
|                                         :confirm => confirm,
 | |
|                                         :pin     => pin
 | |
|                                     }
 | |
|                                 ));
 | |
|                             }
 | |
|                         }
 | |
|                     } else if (type.equals("info") && content != null) {
 | |
|                         // Cannot exit from a non-actionable information only menu item.
 | |
|                         addItem(HomeAssistantMenuItemFactory.create().tap(
 | |
|                             name,
 | |
|                             entity,
 | |
|                             content,
 | |
|                             service,
 | |
|                             data,
 | |
|                             {
 | |
|                                 :exit    => false,
 | |
|                                 :confirm => confirm,
 | |
|                                 :pin     => pin
 | |
|                             }
 | |
|                         ));
 | |
|                     } else if (type.equals("group")) {
 | |
|                         addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Return a list of items that need to be updated within this menu structure.
 | |
|     //!
 | |
|     //! MN. Lang.Array.addAll() fails structural type checking without including "Null" in the return type
 | |
|     //!
 | |
|     //! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state.
 | |
|     //
 | |
|     function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem  or HomeAssistantNumericMenuItem or Null> {
 | |
|         var fullList = [];
 | |
|         var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
 | |
| 
 | |
|         for(var i = 0; i < mItems.size(); i++) {
 | |
|             var item = lmi[i];
 | |
|             if (item instanceof HomeAssistantGroupMenuItem) {
 | |
|                 // Group menu items can now have an optional template to evaluate
 | |
|                 var gmi = item as HomeAssistantGroupMenuItem;
 | |
|                 if (gmi.hasTemplate()) {
 | |
|                     fullList.add(item);
 | |
|                 }
 | |
|                 fullList.addAll(item.getMenuView().getItemsToUpdate());
 | |
|             } else if (item instanceof HomeAssistantNumericMenuItem) {
 | |
|                 // Numeric items can have an optional template to evaluate
 | |
|                 var nmi = item as HomeAssistantNumericMenuItem;
 | |
|                 if (nmi.hasTemplate()) {
 | |
|                     fullList.add(item);
 | |
|                 }
 | |
|             } else if (item instanceof HomeAssistantToggleMenuItem) {
 | |
|                 fullList.add(item);
 | |
|             } else if (item instanceof HomeAssistantTapMenuItem) {
 | |
|                 var tmi = item as HomeAssistantTapMenuItem;
 | |
|                 if (tmi.hasTemplate()) {
 | |
|                     fullList.add(item);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return fullList;
 | |
|     }
 | |
| 
 | |
|     //! Called when this View is brought to the foreground. Restore
 | |
|     //! the state of this View and prepare it to be shown. This includes
 | |
|     //! loading resources into memory.
 | |
|     //
 | |
|     function onShow() as Void {}
 | |
| }
 | |
| 
 | |
| //! Delegate for the HomeAssistantView.
 | |
| //!
 | |
| //! Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
 | |
| //
 | |
| class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
 | |
|     private var mIsRootMenuView as Lang.Boolean = false;
 | |
|     private var mTimer          as QuitTimer;
 | |
| 
 | |
|     //! Class Constructor
 | |
|     //!
 | |
|     //! @param isRootMenuView As menus can be nested, this state marks the top level menu so that the
 | |
|     //!                       back event can exit the application completely rather than just popping
 | |
|     //!                       a menu view.
 | |
|     //
 | |
|     function initialize(isRootMenuView as Lang.Boolean) {
 | |
|         Menu2InputDelegate.initialize();
 | |
|         mIsRootMenuView = isRootMenuView;
 | |
|         mTimer          = getApp().getQuitTimer();
 | |
|     }
 | |
| 
 | |
|     //! Handle the back button (ESC)
 | |
|     //
 | |
|     function onBack() {
 | |
|         mTimer.reset();
 | |
| 
 | |
|         if (mIsRootMenuView) {
 | |
|             // If its started from glance or as an activity, directly exit the widget/app
 | |
|             // (on widgets without glance, this exit() won't do anything,
 | |
|             // so the base view will be shown instead, through the popView below this "if body")
 | |
|             System.exit();  
 | |
|         }
 | |
| 
 | |
|         WatchUi.popView(WatchUi.SLIDE_RIGHT);
 | |
|     }
 | |
| 
 | |
|     //! Only for CheckboxMenu
 | |
|     //
 | |
|     function onDone() {
 | |
|         mTimer.reset();
 | |
|     }
 | |
| 
 | |
|     //! Only for CustomMenu
 | |
|     //
 | |
|     function onFooter() {
 | |
|         mTimer.reset();
 | |
|     }
 | |
| 
 | |
|     //! Select event
 | |
|     //!
 | |
|     //! @param item Selected menu item.
 | |
|     //
 | |
|     function onSelect(item as WatchUi.MenuItem) as Void {
 | |
|         mTimer.reset();
 | |
|         if (item instanceof HomeAssistantToggleMenuItem) {
 | |
|             var haToggleItem = item as HomeAssistantToggleMenuItem;
 | |
|             // System.println(haToggleItem.getLabel() + " " + haToggleItem.getId() + " " + haToggleItem.isEnabled());
 | |
|             haToggleItem.callService(haToggleItem.isEnabled());
 | |
|         } else if (item instanceof HomeAssistantTapMenuItem) {
 | |
|             var haItem = item as HomeAssistantTapMenuItem;
 | |
|             // System.println(haItem.getLabel() + " " + haItem.getId());
 | |
|             haItem.callService();
 | |
|         } else if (item instanceof HomeAssistantNumericMenuItem) {
 | |
|             var haItem = item as HomeAssistantNumericMenuItem;
 | |
|             // System.println(haItem.getLabel() + " " + haItem.getId());
 | |
|             // create new view to select new value
 | |
|             var mPickerFactory  = new HomeAssistantNumericFactory((haItem as HomeAssistantNumericMenuItem).getPicker());
 | |
|             var mPicker         = new HomeAssistantNumericPicker(mPickerFactory,haItem);
 | |
|             var mPickerDelegate = new HomeAssistantNumericPickerDelegate(mPicker);
 | |
|             WatchUi.pushView(mPicker,mPickerDelegate,WatchUi.SLIDE_LEFT);
 | |
|         } else if (item instanceof HomeAssistantGroupMenuItem) {
 | |
|             var haMenuItem = item as HomeAssistantGroupMenuItem;
 | |
|             WatchUi.pushView(haMenuItem.getMenuView(), new HomeAssistantViewDelegate(false), WatchUi.SLIDE_LEFT);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     //! Only for CustomMenu
 | |
|     //
 | |
|     function onTitle() {
 | |
|         mTimer.reset();
 | |
|     }
 | |
| 
 | |
| }
 |