From 52e2efddd85705bd2d7193e15edfa5e662fbca31 Mon Sep 17 00:00:00 2001 From: thmichel Date: Fri, 10 Oct 2025 11:46:20 +0200 Subject: [PATCH 01/21] Added numeric Menu Item --- .gitignore | 2 + .vscode/launch.json | 8 - manifest.xml | 244 ++++++------------------- source/HomeAssistantApp.mc | 4 +- source/HomeAssistantMenuItemFactory.mc | 43 ++++- source/HomeAssistantNumericMenuItem.mc | 231 +++++++++++++++++++++++ source/HomeAssistantNumericView.mc | 212 +++++++++++++++++++++ source/HomeAssistantView.mc | 30 ++- source/Settings.mc | 12 +- 9 files changed, 578 insertions(+), 208 deletions(-) create mode 100644 source/HomeAssistantNumericMenuItem.mc create mode 100644 source/HomeAssistantNumericView.mc diff --git a/.gitignore b/.gitignore index 320a50f..2a0053d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ Thumbs.db source/ClientId.mc # Gemini API key for automated translations gemini_api_key.txt +# URLs ans settings modified for testing, so don't sync +sourc/Settings.mc \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index a93993a..070367e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,14 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "justMyCode": true - }, { "type": "monkeyc", "request": "launch", diff --git a/manifest.xml b/manifest.xml index d17e98b..2f6bc8f 100644 --- a/manifest.xml +++ b/manifest.xml @@ -20,211 +20,77 @@ --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ara - bul - ces - dan - deu - dut - eng - est - fin - fre - gre - heb - hrv - hun - ind - ita - jpn - kor - lav - lit - nob - pol - por - ron - - slo - slv - spa - swe - tha - tur - ukr - vie - zhs - zht - zsm - - + slo + slv + spa + swe + tha + tur + ukr + vie + zhs + zht + zsm + + - - + + \ No newline at end of file diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 9bbb5d0..6f628c7 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -30,14 +30,14 @@ class HomeAssistantApp extends Application.AppBase { private var mHasToast as Lang.Boolean = false; private var mApiStatus as Lang.String?; private var mMenuStatus as Lang.String?; - private var mHaMenu as HomeAssistantView?; + private var mHaMenu as HomeAssistantView?; private var mGlanceTemplate as Lang.String? = null; private var mGlanceText as Lang.String? = null; private var mQuitTimer as QuitTimer?; private var mGlanceTimer as Timer.Timer?; private var mUpdateTimer as Timer.Timer?; // Array initialised by onReturnFetchMenuConfig() - private var mItemsToUpdate as Lang.Array?; + private var mItemsToUpdate as Lang.Array?; private var mIsApp as Lang.Boolean = false; // Or Widget private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates private var mTemplates as Lang.Dictionary? = null; // Cache of compiled templates diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc index 35d7bc0..46e5f7b 100644 --- a/source/HomeAssistantMenuItemFactory.mc +++ b/source/HomeAssistantMenuItemFactory.mc @@ -148,7 +148,48 @@ class HomeAssistantMenuItemFactory { ); } } - + //! Numeric menu item. + //! + //! @param definition Items array from the JSON that defines this sub menu. + //! @param template Template for Home Assistant to render (optional) + // + function numeric( + label as Lang.String or Lang.Symbol, + entity_id as Lang.String?, + template as Lang.String?, + service as Lang.String?, + data as Lang.Dictionary?, + options as { + :exit as Lang.Boolean, + :confirm as Lang.Boolean, + :pin as Lang.Boolean, + :icon as WatchUi.Bitmap + } + ) as WatchUi.MenuItem { + if (entity_id != null) { + if (data == null) { + data = { "entity_id" => entity_id }; + + } else { + data.put("entity_id", entity_id); + } + } + var keys = mMenuItemOptions.keys(); + for (var i = 0; i < keys.size(); i++) { + options.put(keys[i], mMenuItemOptions.get(keys[i])); + } + options.put(:icon, mTapTypeIcon); + + return new HomeAssistantNumericMenuItem( + label, + entity_id, + template, + service, + data, + options, + mHomeAssistantService + ); + } //! Group menu item. //! //! @param definition Items array from the JSON that defines this sub menu. diff --git a/source/HomeAssistantNumericMenuItem.mc b/source/HomeAssistantNumericMenuItem.mc new file mode 100644 index 0000000..2533228 --- /dev/null +++ b/source/HomeAssistantNumericMenuItem.mc @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------------- +// +// 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, 31 October 2023 +// +//----------------------------------------------------------------------------------- + +using Toybox.Lang; +using Toybox.WatchUi; +using Toybox.Graphics; + + +//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders +//! a Home Assistant Template. +// +class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { + private var mHomeAssistantService as HomeAssistantService?; + private var mService as Lang.String?; + private var mConfirm as Lang.Boolean; + private var mExit as Lang.Boolean; + private var mPin as Lang.Boolean; + private var mData as Lang.Dictionary?; + private var mStep as Lang.Float=1.0; + private var mValueChanged as Lang.Boolean = false; + private var mValue as Lang.Float?; + private var mEntity as Lang.String?; + private var mFormatString as Lang.String="%.1f"; + + + //! Class Constructor + //! + //! @param label Menu item label. + //! @param template Menu item template. + //! @param service Menu item service. + //! @param data Data to supply to the service call. + //! @param exit Should the service call complete and then exit? + //! @param confirm Should the service call be confirmed to avoid accidental invocation? + //! @param pin Should the service call be protected with a PIN for some low level of security? + //! @param icon Icon to use for the menu item. + //! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin. + //! @param haService Shared Home Assistant service object that will perform the required call. Only + //! one of these objects is created for all menu items to re-use. + // + function initialize( + label as Lang.String or Lang.Symbol, + entity as Lang.String, + template as Lang.String, + service as Lang.String?, + data as Lang.Dictionary?, + options as { + :alignment as WatchUi.MenuItem.Alignment, + :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol, + :exit as Lang.Boolean, + :confirm as Lang.Boolean, + :pin as Lang.Boolean + }?, + haService as HomeAssistantService + ) { + mService = service; + mData = data; + mExit = options[:exit]; + mConfirm = options[:confirm]; + mPin = options[:pin]; + mLabel = label; + mHomeAssistantService = haService; + mEntity = entity; + + HomeAssistantMenuItem.initialize( + label, + template, + { + :alignment => options[:alignment], + :icon => options[:icon] + } + ); + + + + if (mData.get("step") != null) { + mStep = mData.get("step").toString().toFloat(); + } + + if (mData.get("formatString") != null) { + mFormatString=mData.get("formatString").toString(); + } + + } + + + function callService() as Void { + if (!mValueChanged) { return; } + var hasTouchScreen = System.getDeviceSettings().isTouchScreen; + if (mPin && hasTouchScreen) { + var pin = Settings.getPin(); + if (pin != null) { + var pinConfirmationView = new HomeAssistantPinConfirmationView(); + WatchUi.pushView( + pinConfirmationView, + new HomeAssistantPinConfirmationDelegate({ + :callback => method(:onConfirm), + :pin => pin, + :state => false, + :view => pinConfirmationView, + }), + WatchUi.SLIDE_IMMEDIATE + ); + } + } else if (mConfirm) { + if ((! System.getDeviceSettings().phoneConnected || + ! System.getDeviceSettings().connectionAvailable) && + Settings.getWifiLteExecutionEnabled()) { + var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String; + var dialog = new WatchUi.Confirmation(dialogMsg); + WatchUi.pushView( + dialog, + new WifiLteExecutionConfirmDelegate({ + :type => "service", + :service => mService, + :data => mData, + :exit => mExit, + }, dialog), + WatchUi.SLIDE_LEFT + ); + } else { + var view = new HomeAssistantConfirmation(); + WatchUi.pushView( + view, + new HomeAssistantConfirmationDelegate({ + :callback => method(:onConfirm), + :confirmationView => view, + :state => false, + }), + WatchUi.SLIDE_IMMEDIATE + ); + } + } else { + onConfirm(false); + } + } + + + //! Callback function after the menu items selection has been (optionally) confirmed. + //! + //! @param b Ignored. It is included in order to match the expected function prototype of the callback method. + // + function onConfirm(b as Lang.Boolean) as Void { + if (mService != null) { + mHomeAssistantService.call(mService, {"entity_id" => mEntity,mData.get("valueLabel").toString() => mValue}, mExit); + } + } + + + ///! Increase value when Up button is pressed or touch screen swipe down + + function increaseValue() as Void { + if (mValueChanged) + { + mValue += mStep; + } + else { + mValue= getSubLabel().toFloat() + mStep; + mValueChanged=true; + } + setSubLabel(mValue.format(mFormatString)); + } + + ///! Decrease value when Down button is pressed or touch screen swipe up + function decreaseValue() as Void { + if (mValueChanged) + { + mValue -= mStep; + } + else { + mValue= getSubLabel().toFloat() - mStep; + mValueChanged=true; + } + setSubLabel(mValue.format(mFormatString)); + } + + //! Update the menu item's sub label to display the template rendered by Home Assistant. + //! + //! @param data The rendered template (typically a string) to be placed in the sub label. This may + //! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such. + // + function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { + // If vlue has changed, don't use value from HomeAssitant but display target value + if (mValueChanged) { + setSubLabel(mValue.format(mFormatString)); + WatchUi.requestUpdate(); + return; + } + if (data == null) { + setSubLabel($.Rez.Strings.Empty); + } else if(data instanceof Lang.String) { + setSubLabel(data); + } else if(data instanceof Lang.Number) { + var d = data as Lang.Number; + setSubLabel(d.format("%d")); + } else if(data instanceof Lang.Float) { + var f = data as Lang.Float; + setSubLabel(f.format(mFormatString)); + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAssistantMenuItem 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, Number or Float, or the item cannot be formatted locally without error. + setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); + } + WatchUi.requestUpdate(); + } + + //! Set the mValuChanged value. + //! + //! Can be used to reenable update of subLabel + // + function setValueChanged(b as Lang.Boolean) as Void { + mValueChanged = b; + } +} diff --git a/source/HomeAssistantNumericView.mc b/source/HomeAssistantNumericView.mc new file mode 100644 index 0000000..2d874dc --- /dev/null +++ b/source/HomeAssistantNumericView.mc @@ -0,0 +1,212 @@ +//----------------------------------------------------------------------------------- +// +// 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; + + +using Toybox.Application.Properties; +using Toybox.Timer; + + +//! Home Assistant menu construction. +// +class HomeAssistantNumericView extends WatchUi.Menu2 { + + private var mMenuItem as HomeAssistantNumericMenuItem; + + + //! Class Constructor + // + function initialize( + menuItem as HomeAssistantNumericMenuItem + + ) { + mMenuItem = menuItem; + + WatchUi.Menu2.initialize({:title => mMenuItem.getLabel()}); + + addItem(mMenuItem); + + //updateState(mData); + + } + + //! Return the menu item + //! + //! @return A HomeAssitantTapMenuItem (or null). + // + function getMenuItem() as HomeAssistantNumericMenuItem? { + return mMenuItem; + } + + //! Update the menu item's sub label to display the template rendered by Home Assistant. + //! + //! @param data The rendered template (typically a string) to be placed in the sub label. This may + //! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such. + // + function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { + if (data == null) { + mMenuItem.setSubLabel($.Rez.Strings.Empty); + } else if(data instanceof Lang.String) { + mMenuItem.setSubLabel(data); + } else if(data instanceof Lang.Number) { + var d = data as Lang.Number; + mMenuItem.setSubLabel(d.format("%d")); + } else if(data instanceof Lang.Float) { + var f = data as Lang.Float; + mMenuItem.setSubLabel(f.format("%f")); + } else if(data instanceof Lang.Dictionary) { + // System.println("HomeAssistantMenuItem updateState() data = " + data); + if (data.get("error") != null) { + mMenuItem.setSubLabel($.Rez.Strings.TemplateError); + } else { + mMenuItem.setSubLabel($.Rez.Strings.PotentialError); + } + } else { + // The template must return a Lang.String, Number or Float, or the item cannot be formatted locally without error. + mMenuItem.setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); + } + WatchUi.requestUpdate(); + } + + + //! 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 { + var fullList = []; + var lmi = mItems as Lang.Array; + + 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 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 HomeAssistantNumericViewDelegate extends WatchUi.Menu2InputDelegate { + private var mIsRootMenuView as Lang.Boolean = false; + private var mTimer as QuitTimer; + private var mItem as HomeAssistantNumericMenuItem; + + //! 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. + //tap + function initialize(isRootMenuView as Lang.Boolean, item as HomeAssistantNumericMenuItem) { + Menu2InputDelegate.initialize(); + mIsRootMenuView = isRootMenuView; + mTimer = getApp().getQuitTimer(); + mItem = item; + } + + //! Handle the back button (ESC) + // + function onBack() { + mTimer.reset(); + + mItem.setValueChanged(false); + + 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(); + } + + // Decrease Value + function onNextPage() as Lang.Boolean { + mItem.decreaseValue(); + return true; + } + //Increase Value + function onPreviousPage() as Lang.Boolean { + mItem.increaseValue(); + return true; + } + + + //! Select event + //! + //! @param item Selected menu item. + // + function onSelect(item as WatchUi.MenuItem) as Void { + mTimer.reset(); + mItem.callService(); + WatchUi.popView(WatchUi.SLIDE_RIGHT); + return; + } + + //! Only for CustomMenu + // + function onTitle() { + mTimer.reset(); + } + + +} + + diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc index 22cac07..ce028b0 100644 --- a/source/HomeAssistantView.mc +++ b/source/HomeAssistantView.mc @@ -126,6 +126,19 @@ class HomeAssistantView extends WatchUi.Menu2 { } )); } + } else if (type.equals("numeric") && service != null) { + addItem(HomeAssistantMenuItemFactory.create().numeric( + name, + entity, + content, + service, + data, + { + :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( @@ -154,7 +167,8 @@ class HomeAssistantView extends WatchUi.Menu2 { //! //! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state. // - function getItemsToUpdate() as Lang.Array { + + function getItemsToUpdate() as Lang.Array { var fullList = []; var lmi = mItems as Lang.Array; @@ -167,6 +181,12 @@ class HomeAssistantView extends WatchUi.Menu2 { 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) { @@ -216,7 +236,7 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { // 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(); + System.exit(); } WatchUi.popView(WatchUi.SLIDE_RIGHT); @@ -248,6 +268,12 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { 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 valu + var numView = new HomeAssistantNumericView(haItem); + WatchUi.pushView(numView, new HomeAssistantNumericViewDelegate(false,haItem), WatchUi.SLIDE_LEFT); } else if (item instanceof HomeAssistantGroupMenuItem) { var haMenuItem = item as HomeAssistantGroupMenuItem; // System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId()); diff --git a/source/Settings.mc b/source/Settings.mc index 4dc435e..3ab46ed 100644 --- a/source/Settings.mc +++ b/source/Settings.mc @@ -29,10 +29,10 @@ using Toybox.Time; // (:glance, :background) class Settings { - private static var mApiKey as Lang.String? = ""; + private static var mApiKey as Lang.String? = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJlNThlZDk1MjIwMmU0MmYyOTVmODYyMGVjNDQwZDk1MCIsImlhdCI6MTc0MjkwNzc4MSwiZXhwIjoyMDU4MjY3NzgxfQ.kM8pXYPRADMrDGqmYYEloZH50avOWtCSzpoZbC0gze0"; private static var mWebhookId as Lang.String? = ""; - private static var mApiUrl as Lang.String? = ""; - private static var mConfigUrl as Lang.String? = ""; + private static var mApiUrl as Lang.String? = "https://homeassistant.michel.ruhr/api"; + private static var mConfigUrl as Lang.String? = "https://homeassistant.michel.ruhr/local/garmin-test.json"; private static var mCacheConfig as Lang.Boolean = false; private static var mClearCache as Lang.Boolean = false; private static var mMenuCheck as Lang.Boolean = false; @@ -63,10 +63,10 @@ class Settings { // static function update() { mIsApp = getApp().getIsApp(); - mApiKey = Properties.getValue("api_key"); + //mApiKey = Properties.getValue("api_key"); mWebhookId = Properties.getValue("webhook_id"); - mApiUrl = Properties.getValue("api_url"); - mConfigUrl = Properties.getValue("config_url"); + //mApiUrl = Properties.getValue("api_url"); + //mConfigUrl = Properties.getValue("config_url"); mCacheConfig = Properties.getValue("cache_config"); mClearCache = Properties.getValue("clear_cache"); mMenuCheck = Properties.getValue("enable_menu_update_check"); From 2981893af718cee513d904b59874d48caa537ac7 Mon Sep 17 00:00:00 2001 From: thmichel Date: Sat, 11 Oct 2025 21:51:50 +0200 Subject: [PATCH 02/21] Using a Picker to set new value --- source/HomeAssistantMenuItemFactory.mc | 3 +- source/HomeAssistantNumericMenuItem.mc | 74 ++---- source/HomeAssistantNumericView.mc | 212 ------------------ source/HomeAssistantView.mc | 12 +- source/factory/HomeAssistantNumericFactory.mc | 88 ++++++++ source/picker/HomeAssistantNumericPicker.mc | 102 +++++++++ 6 files changed, 216 insertions(+), 275 deletions(-) delete mode 100644 source/HomeAssistantNumericView.mc create mode 100644 source/factory/HomeAssistantNumericFactory.mc create mode 100644 source/picker/HomeAssistantNumericPicker.mc diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc index 46e5f7b..1136d23 100644 --- a/source/HomeAssistantMenuItemFactory.mc +++ b/source/HomeAssistantMenuItemFactory.mc @@ -173,7 +173,7 @@ class HomeAssistantMenuItemFactory { } else { data.put("entity_id", entity_id); } - } + } var keys = mMenuItemOptions.keys(); for (var i = 0; i < keys.size(); i++) { options.put(keys[i], mMenuItemOptions.get(keys[i])); @@ -182,7 +182,6 @@ class HomeAssistantMenuItemFactory { return new HomeAssistantNumericMenuItem( label, - entity_id, template, service, data, diff --git a/source/HomeAssistantNumericMenuItem.mc b/source/HomeAssistantNumericMenuItem.mc index 2533228..19b9ef4 100644 --- a/source/HomeAssistantNumericMenuItem.mc +++ b/source/HomeAssistantNumericMenuItem.mc @@ -28,10 +28,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { private var mExit as Lang.Boolean; private var mPin as Lang.Boolean; private var mData as Lang.Dictionary?; - private var mStep as Lang.Float=1.0; - private var mValueChanged as Lang.Boolean = false; - private var mValue as Lang.Float?; - private var mEntity as Lang.String?; + private var mValue as Lang.String?; private var mFormatString as Lang.String="%.1f"; @@ -51,7 +48,6 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { // function initialize( label as Lang.String or Lang.Symbol, - entity as Lang.String, template as Lang.String, service as Lang.String?, data as Lang.Dictionary?, @@ -71,7 +67,6 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { mPin = options[:pin]; mLabel = label; mHomeAssistantService = haService; - mEntity = entity; HomeAssistantMenuItem.initialize( label, @@ -81,22 +76,11 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { :icon => options[:icon] } ); - - - - if (mData.get("step") != null) { - mStep = mData.get("step").toString().toFloat(); - } - - if (mData.get("formatString") != null) { - mFormatString=mData.get("formatString").toString(); - } - } + function callService() as Void { - if (!mValueChanged) { return; } var hasTouchScreen = System.getDeviceSettings().isTouchScreen; if (mPin && hasTouchScreen) { var pin = Settings.getPin(); @@ -145,45 +129,20 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { onConfirm(false); } } - + + //! Callback function after the menu items selection has been (optionally) confirmed. //! //! @param b Ignored. It is included in order to match the expected function prototype of the callback method. // function onConfirm(b as Lang.Boolean) as Void { - if (mService != null) { - mHomeAssistantService.call(mService, {"entity_id" => mEntity,mData.get("valueLabel").toString() => mValue}, mExit); - } + mHomeAssistantService.call(mService, {"entity_id" => mData.get("entity_id").toString(),mData.get("valueLabel").toString() => mValue}, mExit); + } - ///! Increase value when Up button is pressed or touch screen swipe down - - function increaseValue() as Void { - if (mValueChanged) - { - mValue += mStep; - } - else { - mValue= getSubLabel().toFloat() + mStep; - mValueChanged=true; - } - setSubLabel(mValue.format(mFormatString)); - } - - ///! Decrease value when Down button is pressed or touch screen swipe up - function decreaseValue() as Void { - if (mValueChanged) - { - mValue -= mStep; - } - else { - mValue= getSubLabel().toFloat() - mStep; - mValueChanged=true; - } - setSubLabel(mValue.format(mFormatString)); - } + //! Update the menu item's sub label to display the template rendered by Home Assistant. //! @@ -191,12 +150,6 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { //! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such. // function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { - // If vlue has changed, don't use value from HomeAssitant but display target value - if (mValueChanged) { - setSubLabel(mValue.format(mFormatString)); - WatchUi.requestUpdate(); - return; - } if (data == null) { setSubLabel($.Rez.Strings.Empty); } else if(data instanceof Lang.String) { @@ -221,11 +174,16 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { WatchUi.requestUpdate(); } - //! Set the mValuChanged value. + //! Set the mValue value. //! - //! Can be used to reenable update of subLabel + //! Needed to set new value via the Service call // - function setValueChanged(b as Lang.Boolean) as Void { - mValueChanged = b; + function setValue(value as Lang.String) as Void { + mValue = value; } + + function getData() as Lang.Dictionary { + return mData; + } + } diff --git a/source/HomeAssistantNumericView.mc b/source/HomeAssistantNumericView.mc deleted file mode 100644 index 2d874dc..0000000 --- a/source/HomeAssistantNumericView.mc +++ /dev/null @@ -1,212 +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 & Someone0nEarth & moesterheld, 31 October 2023 -// -//----------------------------------------------------------------------------------- - -using Toybox.Application; -using Toybox.Lang; -using Toybox.Graphics; -using Toybox.System; -using Toybox.WatchUi; - - -using Toybox.Application.Properties; -using Toybox.Timer; - - -//! Home Assistant menu construction. -// -class HomeAssistantNumericView extends WatchUi.Menu2 { - - private var mMenuItem as HomeAssistantNumericMenuItem; - - - //! Class Constructor - // - function initialize( - menuItem as HomeAssistantNumericMenuItem - - ) { - mMenuItem = menuItem; - - WatchUi.Menu2.initialize({:title => mMenuItem.getLabel()}); - - addItem(mMenuItem); - - //updateState(mData); - - } - - //! Return the menu item - //! - //! @return A HomeAssitantTapMenuItem (or null). - // - function getMenuItem() as HomeAssistantNumericMenuItem? { - return mMenuItem; - } - - //! Update the menu item's sub label to display the template rendered by Home Assistant. - //! - //! @param data The rendered template (typically a string) to be placed in the sub label. This may - //! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such. - // - function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { - if (data == null) { - mMenuItem.setSubLabel($.Rez.Strings.Empty); - } else if(data instanceof Lang.String) { - mMenuItem.setSubLabel(data); - } else if(data instanceof Lang.Number) { - var d = data as Lang.Number; - mMenuItem.setSubLabel(d.format("%d")); - } else if(data instanceof Lang.Float) { - var f = data as Lang.Float; - mMenuItem.setSubLabel(f.format("%f")); - } else if(data instanceof Lang.Dictionary) { - // System.println("HomeAssistantMenuItem updateState() data = " + data); - if (data.get("error") != null) { - mMenuItem.setSubLabel($.Rez.Strings.TemplateError); - } else { - mMenuItem.setSubLabel($.Rez.Strings.PotentialError); - } - } else { - // The template must return a Lang.String, Number or Float, or the item cannot be formatted locally without error. - mMenuItem.setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); - } - WatchUi.requestUpdate(); - } - - - //! 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 { - var fullList = []; - var lmi = mItems as Lang.Array; - - 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 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 HomeAssistantNumericViewDelegate extends WatchUi.Menu2InputDelegate { - private var mIsRootMenuView as Lang.Boolean = false; - private var mTimer as QuitTimer; - private var mItem as HomeAssistantNumericMenuItem; - - //! 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. - //tap - function initialize(isRootMenuView as Lang.Boolean, item as HomeAssistantNumericMenuItem) { - Menu2InputDelegate.initialize(); - mIsRootMenuView = isRootMenuView; - mTimer = getApp().getQuitTimer(); - mItem = item; - } - - //! Handle the back button (ESC) - // - function onBack() { - mTimer.reset(); - - mItem.setValueChanged(false); - - 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(); - } - - // Decrease Value - function onNextPage() as Lang.Boolean { - mItem.decreaseValue(); - return true; - } - //Increase Value - function onPreviousPage() as Lang.Boolean { - mItem.increaseValue(); - return true; - } - - - //! Select event - //! - //! @param item Selected menu item. - // - function onSelect(item as WatchUi.MenuItem) as Void { - mTimer.reset(); - mItem.callService(); - WatchUi.popView(WatchUi.SLIDE_RIGHT); - return; - } - - //! Only for CustomMenu - // - function onTitle() { - mTimer.reset(); - } - - -} - - diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc index ce028b0..a9f4d9a 100644 --- a/source/HomeAssistantView.mc +++ b/source/HomeAssistantView.mc @@ -271,12 +271,18 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { } else if (item instanceof HomeAssistantNumericMenuItem) { var haItem = item as HomeAssistantNumericMenuItem; // System.println(haItem.getLabel() + " " + haItem.getId()); - // create new view to select new valu - var numView = new HomeAssistantNumericView(haItem); - WatchUi.pushView(numView, new HomeAssistantNumericViewDelegate(false,haItem), WatchUi.SLIDE_LEFT); + // create new view to select new value + + var mPickerFactory = new HomeAssistantNumericFactory(haItem.getData()); + + var mPicker = new HomeAssistantNumericPicker(mPickerFactory,haItem);//{:pattern => [mPickerFactory}); + var mPickerDelegate = new HomeAssistantNumericPickerDelegate(mPicker); + WatchUi.pushView(mPicker,mPickerDelegate,WatchUi.SLIDE_LEFT); + //WatchUi.pushView(numView, new HomeAssistantNumericViewDelegate(false,haItem), WatchUi.SLIDE_LEFT); } else if (item instanceof HomeAssistantGroupMenuItem) { var haMenuItem = item as HomeAssistantGroupMenuItem; // System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId()); + WatchUi.pushView(haMenuItem.getMenuView(), new HomeAssistantViewDelegate(false), WatchUi.SLIDE_LEFT); // } else { // System.println(item.getLabel() + " " + item.getId()); diff --git a/source/factory/HomeAssistantNumericFactory.mc b/source/factory/HomeAssistantNumericFactory.mc new file mode 100644 index 0000000..8f16b6f --- /dev/null +++ b/source/factory/HomeAssistantNumericFactory.mc @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------------- +// +// 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, 31 October 2023 +// +//------------------------------------------------------------ + +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.WatchUi; + +//! Factory that controls which numbers can be picked +class HomeAssistantNumericFactory extends WatchUi.PickerFactory { + // define default values in case not contained in data + private var mStart as Lang.Float = 0.0; + private var mStop as Lang.Float = 100.0; + private var mStep as Lang.Float = 1.0; + private var mFormatString as Lang.String = "%.2f"; + + //! Class Constructor + //! + public function initialize(data as Lang.Dictionary) { + PickerFactory.initialize(); + + // Get values from data + + var val = data.get("start"); + if (val != null) { + mStart = val.toString().toFloat(); + } + val = data.get("stop"); + if (val != null) { + mStop = val.toString().toFloat(); + } + val = data.get("step"); + if (val != null) { + mStep = val.toString().toFloat(); + } + val = data.get("formatString"); + if (val != null) { + mFormatString = val.toString(); + } + + } + + //! Get the index of a number item + //! @param value The number to get the index of + //! @return The index of the number + public function getIndex(value as Float) as Number { + return ((value / mStep) - mStart).toNumber(); + } + + //! Generate a Drawable instance for an item + //! @param index The item index + //! @param selected true if the current item is selected, false otherwise + //! @return Drawable for the item + public function getDrawable(index as Number, selected as Boolean) as Drawable? { + var value = getValue(index); + var text = "No item"; + if (value instanceof Float) { + text = value.format(mFormatString); + } + return new WatchUi.Text({:text=>text, :color=>Graphics.COLOR_WHITE, + :locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_CENTER}); + } + + //! Get the value of the item at the given index + //! @param index Index of the item to get the value of + //! @return Value of the item + public function getValue(index as Number) as Object? { + return mStart + (index * mStep); + } + + //! Get the number of picker items + //! @return Number of items + public function getSize() as Number { + return ((mStop - mStart) / mStep).toNumber() + 1; + } + +} diff --git a/source/picker/HomeAssistantNumericPicker.mc b/source/picker/HomeAssistantNumericPicker.mc new file mode 100644 index 0000000..a8d7317 --- /dev/null +++ b/source/picker/HomeAssistantNumericPicker.mc @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------------------- +// +// 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, 31 October 2023 +// +//------------------------------------------------------------ + +using Toybox.Application; +using Toybox.Lang; +using Toybox.Graphics; +using Toybox.System; +using Toybox.WatchUi; + +//! Picker that allows the user to choose a float value +class HomeAssistantNumericPicker extends WatchUi.Picker { + + private var mFactory as HomeAssistantNumericFactory; + private var mItem as HomeAssistantNumericMenuItem; + + //! Constructor + public function initialize(factory as HomeAssistantNumericFactory, haItem as HomeAssistantNumericMenuItem) { + + mFactory = factory; + + + var pickerOptions = {:pattern=>[mFactory]}; + mItem=haItem; + + + var data = mItem.getData(); + + var start = 0.0; + var val = data.get("start"); + if (val != null) { + start = val.toString().toFloat(); + } + var step = 1.0; + val = data.get("step"); + if (val != null) { + step = val.toString().toFloat(); + } + val = haItem.getSubLabel().toFloat(); + var index = ((val -start) / step).toNumber(); + + pickerOptions[:defaults] =[index]; + + var title = new WatchUi.Text({:text=>haItem.getLabel(), :locX=>WatchUi.LAYOUT_HALIGN_CENTER, + :locY=>WatchUi.LAYOUT_VALIGN_BOTTOM}); + pickerOptions[:title] = title; + + + Picker.initialize(pickerOptions); + + } + + + //! Get whether the user is done picking + //! @param value Value user selected + //! @return true if user is done, false otherwise + public function onConfirm(value as Lang.String) as Void { + mItem.setValue(value); + mItem.callService(); + } + + +} + +//! Responds to a numeric picker selection or cancellation +class HomeAssistantNumericPickerDelegate extends WatchUi.PickerDelegate { + private var mPicker as HomeAssistantNumericPicker; + + //! Constructor + public function initialize(picker as HomeAssistantNumericPicker) { + PickerDelegate.initialize(); + mPicker = picker; + } + + //! Handle a cancel event from the picker + //! @return true if handled, false otherwise + public function onCancel() as Lang.Boolean { + WatchUi.popView(WatchUi.SLIDE_RIGHT); + return true; + } + + //! Handle a confirm event from the picker + //! @param values The values chosen in the picker + //! @return true if handled, false otherwise + public function onAccept(values as Lang.Array) as Lang.Boolean { + var chosenValue = values[0].toString(); + mPicker.onConfirm(chosenValue); + WatchUi.popView(WatchUi.SLIDE_RIGHT); + return true; + } +} From 9a61c9ce772a2758f83c41bdb7e30dad0a985cdc Mon Sep 17 00:00:00 2001 From: thmichel Date: Sat, 11 Oct 2025 22:00:43 +0200 Subject: [PATCH 03/21] Code cleanup --- source/HomeAssistantNumericMenuItem.mc | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/source/HomeAssistantNumericMenuItem.mc b/source/HomeAssistantNumericMenuItem.mc index 19b9ef4..e04fdd7 100644 --- a/source/HomeAssistantNumericMenuItem.mc +++ b/source/HomeAssistantNumericMenuItem.mc @@ -143,7 +143,6 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { - //! Update the menu item's sub label to display the template rendered by Home Assistant. //! //! @param data The rendered template (typically a string) to be placed in the sub label. This may @@ -152,23 +151,11 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { if (data == null) { setSubLabel($.Rez.Strings.Empty); - } else if(data instanceof Lang.String) { - setSubLabel(data); - } else if(data instanceof Lang.Number) { - var d = data as Lang.Number; - setSubLabel(d.format("%d")); } else if(data instanceof Lang.Float) { var f = data as Lang.Float; setSubLabel(f.format(mFormatString)); - } else if(data instanceof Lang.Dictionary) { - // System.println("HomeAssistantMenuItem 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, Number or Float, or the item cannot be formatted locally without error. + } else { + // The template must return a Float, or the item cannot be formatted locally without error. setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); } WatchUi.requestUpdate(); From 6609fed35df6a997526eb3ac915a6b2bc826397a Mon Sep 17 00:00:00 2001 From: thmichel Date: Sat, 11 Oct 2025 22:28:50 +0200 Subject: [PATCH 04/21] REmoved credentials from settings --- manifest.xml | 3 ++- source/Settings.mc | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/manifest.xml b/manifest.xml index 2f6bc8f..589921f 100644 --- a/manifest.xml +++ b/manifest.xml @@ -24,7 +24,7 @@ Use "Monkey C: Edit Application" from the Visual Studio Code command palette to update the application attributes. --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +