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");