diff --git a/HomeAssistant.code-workspace b/HomeAssistant.code-workspace index 876a149..573bffd 100644 --- a/HomeAssistant.code-workspace +++ b/HomeAssistant.code-workspace @@ -4,5 +4,9 @@ "path": "." } ], - "settings": {} + "settings": { + "cSpell.words": [ + "Initialiser" + ] + } } \ No newline at end of file diff --git a/source/Alert.mc b/source/Alert.mc index 4ef730c..5e3f591 100644 --- a/source/Alert.mc +++ b/source/Alert.mc @@ -11,15 +11,6 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// Alert provides a means to present application notifications to the user -// briefly. Credit to travis.vitek on forums.garmin.com. -// -// Reference: -// * https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -27,6 +18,12 @@ using Toybox.Graphics; using Toybox.WatchUi; using Toybox.Timer; +//! The Alert class provides a means to present application notifications to the user +//! briefly. Credit to travis.vitek on forums.garmin.com. +//! +//! Reference: +//! @url https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages +// class Alert extends WatchUi.View { private static const scRadius = 10; private var mTimer as Timer.Timer; @@ -36,6 +33,16 @@ class Alert extends WatchUi.View { private var mFgcolor as Graphics.ColorType; private var mBgcolor as Graphics.ColorType; + //! Class Constructor + //! @param params A dictionary object as follows:
+ //! {
+ //!   :timeout as Lang.Number, // Timeout in millseconds
+ //!   :font as Graphics.FontType, // Text font size
+ //!   :text as Lang.String, // Text to display
+ //!   :fgcolor as Graphics.ColorType, // Foreground Colour
+ //!   :bgcolor as Graphics.ColorType // Background Colour
+ //! } + // function initialize(params as Lang.Dictionary) { View.initialize(); @@ -67,14 +74,22 @@ class Alert extends WatchUi.View { mTimer = new Timer.Timer(); } + //! Setup a timer to dismiss the alert. + // function onShow() { mTimer.start(method(:dismiss), mTimeout, false); } + //! Prematurely stop the timer. + // function onHide() { mTimer.stop(); } + //! Draw the Alert view. + //! + //! @param dc Device context + // function onUpdate(dc as Graphics.Dc) { var tWidth = dc.getTextWidthInPixels(mText, mFont); var tHeight = dc.getFontHeight(mFont); @@ -110,32 +125,49 @@ class Alert extends WatchUi.View { dc.drawText(tX, tY, mFont, mText, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); } - // Remove the alert from view, usually on user input, but that is defined by the calling function. + //! Remove the alert from view, usually on user input, but that is defined by the calling function. // function dismiss() as Void { WatchUi.popView(SLIDE_IMMEDIATE); } - function pushView(transition) as Void { + //! Push this view onto the view stack. + //! + //! @param transition Slide Type + function pushView(transition as WatchUi.SlideType) as Void { WatchUi.pushView(self, new AlertDelegate(self), transition); } } +//! Input Delegate for the Alert view. +// class AlertDelegate extends WatchUi.InputDelegate { - private var mView; + private var mView as Alert; - function initialize(view) { + //! Class Constructor + //! + //! @param view The Alert view for which this class is a delegate. + //! + function initialize(view as Alert) { InputDelegate.initialize(); mView = view; } - function onKey(evt) as Lang.Boolean { + //! Handle key events. + //! + //! @param evt The key event whose value is ignored, just fact of key event matters. + //! + function onKey(evt as WatchUi.KeyEvent) as Lang.Boolean { mView.dismiss(); getApp().getQuitTimer().reset(); return true; } - function onTap(evt) as Lang.Boolean { + //! Handle click events. + //! + //! @param evt The click event whose value is ignored, just fact of key event matters. + //! + function onTap(evt as WatchUi.ClickEvent) as Lang.Boolean { mView.dismiss(); getApp().getQuitTimer().reset(); return true; diff --git a/source/BackgroundServiceDelegate.mc b/source/BackgroundServiceDelegate.mc index f9bbdfa..7bb3102 100644 --- a/source/BackgroundServiceDelegate.mc +++ b/source/BackgroundServiceDelegate.mc @@ -11,12 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // -// -// Description: -// -// The background service delegate currently just reports the Garmin watch's battery -// level. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -25,20 +19,43 @@ using Toybox.Background; using Toybox.System; using Toybox.Activity; +//! The background service delegate reports the Garmin watch's various status values +//! back to the Home Assistant instance. +// (:background) class BackgroundServiceDelegate extends System.ServiceDelegate { + //! Class Constructor + // function initialize() { ServiceDelegate.initialize(); } - function onReturnBatteryUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { - // System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Code: " + responseCode); - // System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Data: " + data); + //! Callback function for doUpdate(). + //! + //! @param responseCode Response code + //! @param data Return data + // + function onReturnDoUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + // System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Code: " + responseCode); + // System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Data: " + data); Background.exit(null); } - function onActivityCompleted(activity as { :sport as Activity.Sport, :subSport as Activity.SubSport }) as Void { + //! Called on completion of an activity. + //! + //! @param activity Specified as a Dictionary with two items.
+ //! {
+ //!   :sport as Activity.Sport
+ //!   :subSport as Activity.SubSport
+ //! } + // + function onActivityCompleted( + activity as { + :sport as Activity.Sport, + :subSport as Activity.SubSport + } + ) as Void { if (!System.getDeviceSettings().phoneConnected) { // System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call."); } else if (!System.getDeviceSettings().connectionAvailable) { @@ -50,6 +67,8 @@ class BackgroundServiceDelegate extends System.ServiceDelegate { } } + //! Called periodically to send status updates to the Home Assistant instance. + // function onTemporalEvent() as Void { if (!System.getDeviceSettings().phoneConnected) { // System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call."); @@ -76,7 +95,15 @@ class BackgroundServiceDelegate extends System.ServiceDelegate { } } - private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) { + //! Combined update function to collect the data to be sent as updates to the Home Assistant instance. + //! + //! @param activity Activity.Sport + //! @param sub_activity Activity.SubSport + // + private function doUpdate( + activity as Lang.Number or Null, + sub_activity as Lang.Number or Null + ) { // System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call."); var position = Position.getInfo(); // System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees()); @@ -136,7 +163,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate { }, :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON }, - method(:onReturnBatteryUpdate) + method(:onReturnDoUpdate) ); } var activityInfo = ActivityMonitor.getInfo(); @@ -222,7 +249,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate { }, :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON }, - method(:onReturnBatteryUpdate) + method(:onReturnDoUpdate) ); } diff --git a/source/ClientId.mc.unpopulated b/source/ClientId.mc.unpopulated index 236b902..e52d83f 100644 --- a/source/ClientId.mc.unpopulated +++ b/source/ClientId.mc.unpopulated @@ -11,15 +11,12 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// ClientId is somewhere to store personal credentials that should not be shared in -// a separate file that is locally customised to the source code and not commited -// back to GitHub. -// //----------------------------------------------------------------------------------- +//! ClientId is somewhere to store personal credentials that should not be shared in +//! a separate file that is locally customised to the source code and not committed +//! back to GitHub. +// (:glance) class ClientId { static const webLogUrl = "https://..."; diff --git a/source/ErrorView.mc b/source/ErrorView.mc index 9032200..b36f138 100644 --- a/source/ErrorView.mc +++ b/source/ErrorView.mc @@ -11,22 +11,6 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// ErrorView provides a means to present application errors to the user. These -// should not happen of course... but they do, so best make sure errors can be -// reported. -// -// Designed so that a single ErrorView is used for all errors and hence can ensure -// that only the first call to display is honoured until the view is dismissed. -// This compensates for older devices not being able to call WatchUi.getCurrentView() -// due to not supporting API level 3.4.0. -// -// Usage: -// 1) ErrorView.show("Error message"); -// 2) return ErrorView.create("Error message"); // as Lang.Array -// //----------------------------------------------------------------------------------- using Toybox.Graphics; @@ -35,6 +19,19 @@ using Toybox.WatchUi; using Toybox.Communications; using Toybox.Timer; +//! ErrorView provides a means to present application errors to the user. These +//! should not happen of course... but they do, so best make sure errors can be +//! reported. +//! +//! Designed so that a single ErrorView is used for all errors and hence can ensure +//! that only the first call to display is honoured until the view is dismissed. +//! This compensates for older devices not being able to call WatchUi.getCurrentView() +//! due to not supporting API level 3.4.0. +//! +//! Usage: +//! 1) `ErrorView.show("Error message");` +//! 2) `return ErrorView.create("Error message"); // as Lang.Array` +// class ErrorView extends ScalableView { private static const scErrorIconMargin as Lang.Float = 7f; private var mText as Lang.String = ""; @@ -48,9 +45,11 @@ class ErrorView extends ScalableView { private static var instance; private static var mShown as Lang.Boolean = false; + //! Class Constructor + // function initialize() { ScalableView.initialize(); - mDelegate = new ErrorDelegate(self); + mDelegate = new ErrorDelegate(); // Convert the settings from % of screen size to pixels mErrorIconMargin = pixelsForScreen(scErrorIconMargin); mErrorIcon = Application.loadResource(Rez.Drawables.ErrorIcon) as Graphics.BitmapResource; @@ -59,7 +58,10 @@ class ErrorView extends ScalableView { } } - // Load your resources here + //! Construct the view. + //! + //! @param dc Device context + // function onLayout(dc as Graphics.Dc) as Void { var w = dc.getWidth(); @@ -75,7 +77,10 @@ class ErrorView extends ScalableView { }); } - // Update the view + //! Update the view + //! + //! @param dc Device context + // function onUpdate(dc as Graphics.Dc) as Void { var w = dc.getWidth(); if (mAntiAlias) { @@ -87,10 +92,18 @@ class ErrorView extends ScalableView { mTextArea.draw(dc); } + //! Get this view's delegate for processing events. + // function getDelegate() as ErrorDelegate { return mDelegate; } + //! 'Create' (get) the ErrorView instance, intended to make short work of using this class. E.g. + //! + //! `return ErrorView.create("Went wrong!");` + //! + //! @param text The string to display in the ErrorView. + // static function create(text as Lang.String) as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] { if (instance == null) { instance = new ErrorView(); @@ -102,7 +115,10 @@ class ErrorView extends ScalableView { return [instance, instance.getDelegate()]; } - // Create or reuse an existing ErrorView, and pass on the text. + //! Create or reuse an existing ErrorView, and pass on the text. + //! + //! @param text The string to display in the ErrorView. + // static function show(text as Lang.String) as Void { if (!mShown) { create(text); // Ignore returned values @@ -113,6 +129,8 @@ class ErrorView extends ScalableView { } } + //! Pop the view and clean up timers. + // static function unShow() as Void { if (mShown) { WatchUi.popView(WatchUi.SLIDE_DOWN); @@ -126,7 +144,10 @@ class ErrorView extends ScalableView { } } - // Internal show now we're not a static method like 'show()'. + //! Internal show now we're not a static method like 'show()'. + //! + //! @param text Change the string tio display in the ErrorView. + // function setText(text as Lang.String) as Void { mText = text; if (mTextArea != null) { @@ -137,12 +158,19 @@ class ErrorView extends ScalableView { } + +//! Delegate for the ErrorView. +// class ErrorDelegate extends WatchUi.BehaviorDelegate { - function initialize(view as ErrorView) { + //! Class Constructor + //! + function initialize() { WatchUi.BehaviorDelegate.initialize(); } + //! Process the event to clear the ErrorView. + // function onBack() as Lang.Boolean { getApp().getQuitTimer().reset(); ErrorView.unShow(); diff --git a/source/Globals.mc b/source/Globals.mc index 9d9600c..22baf56 100644 --- a/source/Globals.mc +++ b/source/Globals.mc @@ -11,30 +11,38 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Home Assistant centralised constants. -// //----------------------------------------------------------------------------------- using Toybox.Lang; +//! Home Assistant centralised constants. +// (:glance) class Globals { + //! Alert is a toast at the top of the watch screen, it stays present until tapped + //! or this timeout has expired. static const scAlertTimeout = 2000; // ms - static const scTapTimeout = 1000; // ms - // Time to let the existing HTTP responses get serviced after a - // Communications.NETWORK_RESPONSE_OUT_OF_MEMORY response code. + + //! Time to let the existing HTTP responses get serviced after a + //! `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` response code. static const scApiBackoff = 1000; // ms - // Needs to be long enough to enable a "double ESC" to quit the application from - // an ErrorView. + + //! Needs to be long enough to enable a "double ESC" to quit the application from + //! an ErrorView. static const scApiResume = 200; // ms - // Warn the user after fetching the menu if their watch is low on memory before the device crashes. + + //! Warn the user after fetching the menu if their watch is low on memory before the device crashes. static const scLowMem = 0.90; // percent as a fraction. - // Constants for PIN confirmation dialog - static const scPinMaxFailures = 5; // Maximum number of failed PIN confirmation attemps allwed in ... - static const scPinMaxFailureMinutes = 2; // ... this number of minutes before PIN confirmation is locked for ... - static const scPinLockTimeMinutes = 10; // ... this number of minutes + //! Constant for PIN confirmation dialog.
+ //! Maximum number of failed PIN confirmation attempts allowed in `scPinMaxFailureMinutes`. + static const scPinMaxFailures = 5; + + //! Constant for PIN confirmation dialog.
+ //! Period in minutes during which no more than `scPinMaxFailures` PIN attempts are tolerated. + static const scPinMaxFailureMinutes = 2; + + //! Constant for PIN confirmation dialog.
+ //! Lock out time in minutes after a failed PIN entry. + static const scPinLockTimeMinutes = 10; } diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index d223844..a28209b 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -11,11 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Application root for GarminHomeAssistant -// //----------------------------------------------------------------------------------- using Toybox.Application; @@ -25,6 +20,8 @@ using Toybox.System; using Toybox.Application.Properties; using Toybox.Timer; +//! Application root for GarminHomeAssistant +// (:glance, :background) class HomeAssistantApp extends Application.AppBase { private var mApiStatus as Lang.String or Null; @@ -40,6 +37,8 @@ class HomeAssistantApp extends Application.AppBase { private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates private var mTemplates as Lang.Dictionary = {}; + //! Class Constructor + // function initialize() { AppBase.initialize(); // ATTENTION when adding stuff into this block: @@ -55,7 +54,10 @@ class HomeAssistantApp extends Application.AppBase { // with "(:glance)". } - // onStart() is called on application start up + //! Called on application start up + //! + //! @param state see `AppBase.onStart()` + // function onStart(state as Lang.Dictionary?) as Void { AppBase.onStart(state); // ATTENTION when adding stuff into this block: @@ -71,7 +73,11 @@ class HomeAssistantApp extends Application.AppBase { // with "(:glance)". } - // onStop() is called when your application is exiting + //! Called when your application is exiting + // + //! + //! @param state see `AppBase.onStop()` + // function onStop(state as Lang.Dictionary?) as Void { AppBase.onStop(state); // ATTENTION when adding stuff into this block: @@ -87,7 +93,10 @@ class HomeAssistantApp extends Application.AppBase { // with "(:glance)". } - // Return the initial view of your application here + //! Returns the initial view of the application. + //! + //! @return The initial view. + // function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] { mIsApp = true; mQuitTimer = new QuitTimer(); @@ -132,10 +141,16 @@ class HomeAssistantApp extends Application.AppBase { } } - // Callback function after completing the GET request to fetch the configuration menu. + //! Callback function after completing the GET request to fetch the configuration menu. + //! + //! @param responseCode Response code. + //! @param data Response data. // (:glance) - function onReturnFetchMenuConfig(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + function onReturnFetchMenuConfig( + responseCode as Lang.Number, + data as Null or Lang.Dictionary or Lang.String + ) as Void { // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode); // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data); @@ -208,8 +223,11 @@ class HomeAssistantApp extends Application.AppBase { WatchUi.requestUpdate(); } - // Return true if the menu came from the cache, otherwise false. This is because fetching the menu when not in the cache is - // asynchronous and affects how the views are managed. + //! Fetch the menu configuration over HTTPS, which might be locally cached. + //! + //! @return Return true if the menu came from the cache, otherwise false. This is because fetching + //! the menu when not in the cache is asynchronous and affects how the views are managed. + // (:glance) function fetchMenuConfig() as Lang.Boolean { // System.println("Menu URL = " + Settings.getConfigUrl()); @@ -263,6 +281,10 @@ class HomeAssistantApp extends Application.AppBase { return false; } + //! Build the menu and store in `mHaMenu`. Then start updates if necessary. + //! + //! @param menu The dictionary derived from the JSON menu fetched by `fetchMenuConfig()`. + // private function buildMenu(menu as Lang.Dictionary) { mHaMenu = new HomeAssistantView(menu, null); mQuitTimer.begin(); @@ -271,6 +293,8 @@ class HomeAssistantApp extends Application.AppBase { } // If not, this will be done via a chain in Settings.webhook() and mWebhookManager.requestWebhookId() that registers the sensors. } + //! Start the periodic menu updates for as long as the application is running. + // function startUpdates() { if (mHaMenu != null and !mUpdating) { // Start the continuous update process that continues for as long as the application is running. @@ -279,7 +303,15 @@ class HomeAssistantApp extends Application.AppBase { } } - function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void { + //! Callback function for each menu update GET request. + //! + //! @param responseCode Response code. + //! @param data Response data. + // + function onReturnUpdateMenuItems( + responseCode as Lang.Number, + data as Null or Lang.Dictionary + ) as Void { // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode); // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data); @@ -353,6 +385,8 @@ class HomeAssistantApp extends Application.AppBase { setApiStatus(status); } + //! Construct the GET request to update all menu items. + // function updateMenuItems() as Void { if (! System.getDeviceSettings().phoneConnected) { // System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call."); @@ -368,17 +402,17 @@ class HomeAssistantApp extends Application.AppBase { mTemplates = {}; for (var i = 0; i < mItemsToUpdate.size(); i++) { var item = mItemsToUpdate[i]; - var template = item.buildTemplate(); + var template = item.getTemplate(); if (template != null) { mTemplates.put(i.toString(), { "template" => template }); } - if (item instanceof HomeAssistantToggleMenuItem) { - mTemplates.put(i.toString() + "t", { - "template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate() - }); - } + if (item instanceof HomeAssistantToggleMenuItem) { + mTemplates.put(i.toString() + "t", { + "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate() + }); + } } } // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates @@ -402,10 +436,16 @@ class HomeAssistantApp extends Application.AppBase { } } - // Callback function after completing the GET request to fetch the API status. + //! Callback function after completing the GET request to fetch the API status. + //! + //! @param responseCode Response code. + //! @param data Response data. // (:glance) - function onReturnFetchApiStatus(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + function onReturnFetchApiStatus( + responseCode as Lang.Number, + data as Null or Lang.Dictionary or Lang.String + ) as Void { // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: " + responseCode); // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data); @@ -466,6 +506,8 @@ class HomeAssistantApp extends Application.AppBase { WatchUi.requestUpdate(); } + //! Construct the GET request to test the API status, is it accessible? + // (:glance) function fetchApiStatus() as Void { // System.println("API URL = " + Settings.getApiUrl()); @@ -506,30 +548,49 @@ class HomeAssistantApp extends Application.AppBase { } } + //! Record the API status result. + //! + //! @param s A string describing the API status + // function setApiStatus(s as Lang.String) { mApiStatus = s; } + //! Return the API status result. + //! + //! @return A string describing the API status + // (:glance) function getApiStatus() as Lang.String { return mApiStatus; } + //! Return the Menu status result. + //! + //! @return A string describing the Menu status + // (:glance) function getMenuStatus() as Lang.String { return mMenuStatus; } + //! Return the Menu construction status. + //! + //! @return A Boolean indicating if the menu is loaded into the application. + // function isHomeAssistantMenuLoaded() as Lang.Boolean { return mHaMenu != null; } + //! Make the menu visible on the watch face. + // function pushHomeAssistantMenuView() as Void { WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE); } - // Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take - // alternative action if the test fails. + //! Force status updates. Only take action if `Settings.getPollDelay() > 0`. This must be tested + //! locally as it is then efficient to take alternative action if the test fails. + // function forceStatusUpdates() as Void { // Don't mess with updates unless we are using a timer. if (Settings.getPollDelay() > 0) { @@ -539,10 +600,18 @@ class HomeAssistantApp extends Application.AppBase { } } + //! Return the timer used to quit the application. + //! + //! @return Timer object + // function getQuitTimer() as QuitTimer { return mQuitTimer; } + //! Return the glance view. + //! + //! @return The glance view + // function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null { mIsGlance = true; mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String; @@ -554,30 +623,38 @@ class HomeAssistantApp extends Application.AppBase { return [new HomeAssistantGlanceView(self)]; } - // Required for the Glance update timer. + //! Update the menu and API statuses. Required for the Glance update timer. + // function updateStatus() as Void { mGlanceTimer = null; fetchMenuConfig(); fetchApiStatus(); } + //! Code for when the application settings are updated. + // function onSettingsChanged() as Void { // System.println("HomeAssistantApp onSettingsChanged()"); Settings.update(); } - // Called each time the Registered Temporal Event is to be invoked. So the object is created each time on request and - // then destroyed on completion (to save resources). + //! Called each time the Registered Temporal Event is to be invoked. So the object is created each time + //! on request and then destroyed on completion (to save resources). + // function getServiceDelegate() as [ System.ServiceDelegate ] { return [new BackgroundServiceDelegate()]; } + //! Determine is we are a glance or the full application. Glances should be considered to be separate applications. + // function getIsApp() as Lang.Boolean { return mIsApp; } } +//! Global function to return the application object. +// (:glance, :background) function getApp() as HomeAssistantApp { return Application.getApp() as HomeAssistantApp; diff --git a/source/HomeAssistantConfirmation.mc b/source/HomeAssistantConfirmation.mc index c3b7f27..9b48714 100644 --- a/source/HomeAssistantConfirmation.mc +++ b/source/HomeAssistantConfirmation.mc @@ -11,11 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023 // -// -// Description: -// -// Calling a Home Assistant confirmation dialogue view. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -25,19 +20,27 @@ using Toybox.WatchUi; using Toybox.Timer; using Toybox.Application.Properties; +//! Calling a Home Assistant confirmation dialogue view. +// class HomeAssistantConfirmation extends WatchUi.Confirmation { + //! Class Constructor + // function initialize() { WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String); } } +//! Delegate to respond to the confirmation request. +// class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { private var mConfirmMethod as Method(state as Lang.Boolean) as Void; private var mTimer as Timer.Timer or Null; private var mState as Lang.Boolean; + //! Class Constructor + // function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) { WatchUi.ConfirmationDelegate.initialize(); mConfirmMethod = callback; @@ -49,7 +52,12 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { } } - function onResponse(response) as Lang.Boolean { + //! Respond to the confirmation event. + //! + //! @param response code + //! @return Required to meet the function prototype, but the base class does not indicate a definition. + // + function onResponse(response as WatchUi.Confirm) as Lang.Boolean { getApp().getQuitTimer().reset(); if (mTimer != null) { mTimer.stop(); @@ -60,6 +68,7 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { return true; } + //! Function supplied to a timer in order to limit the time for which the confirmation can be provided. function onTimeout() as Void { mTimer.stop(); WatchUi.popView(WatchUi.SLIDE_RIGHT); diff --git a/source/HomeAssistantGlanceView.mc b/source/HomeAssistantGlanceView.mc index 0d26204..1a29822 100644 --- a/source/HomeAssistantGlanceView.mc +++ b/source/HomeAssistantGlanceView.mc @@ -11,17 +11,14 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 23 November 2023 // -// -// Description: -// -// Glance view for GarminHomeAssistant -// //----------------------------------------------------------------------------------- using Toybox.Lang; using Toybox.WatchUi; using Toybox.Graphics; +//! Glance view for GarminHomeAssistant +// (:glance) class HomeAssistantGlanceView extends WatchUi.GlanceView { private static const scLeftMargin = 5; // in pixels @@ -34,6 +31,8 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView { private var mMenuStatus as WatchUi.Text or Null; private var mAntiAlias as Lang.Boolean = false; + //! Class Constructor + // function initialize(app as HomeAssistantApp) { GlanceView.initialize(); mApp = app; @@ -42,6 +41,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView { } } + //! Construct the view. + //! + //! @param dc Device context + // function onLayout(dc as Graphics.Dc) as Void { var h = dc.getHeight(); var tw = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String, Graphics.FONT_XTINY); @@ -89,6 +92,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView { }); } + //! Update the view with the latest status text. + //! + //! @param dc Device context + // function onUpdate(dc as Graphics.Dc) as Void { GlanceView.onUpdate(dc); if(mAntiAlias) { diff --git a/source/HomeAssistantGroupMenuItem.mc b/source/HomeAssistantGroupMenuItem.mc index f4df2a7..c735659 100644 --- a/source/HomeAssistantGroupMenuItem.mc +++ b/source/HomeAssistantGroupMenuItem.mc @@ -11,20 +11,19 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // -// -// Description: -// -// Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders -// a Home Assistant Template. -// //----------------------------------------------------------------------------------- using Toybox.Lang; using Toybox.WatchUi; +//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders +//! a Home Assistant Template. +// class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem { private var mMenu as HomeAssistantView; + //! Class Constructor + // function initialize( definition as Lang.Dictionary, template as Lang.String, @@ -48,6 +47,8 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem { mMenu = new HomeAssistantView(definition, null); } + //! Return the submenu for this group menu item. + // function getMenuView() as HomeAssistantView { return mMenu; } diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc index 7a185e5..fe7a24f 100644 --- a/source/HomeAssistantMenuItem.mc +++ b/source/HomeAssistantMenuItem.mc @@ -11,20 +11,23 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // -// -// Description: -// -// Generic menu button with an icon that optionally renders a Home Assistant Template. -// //----------------------------------------------------------------------------------- using Toybox.Lang; using Toybox.WatchUi; using Toybox.Graphics; +//! Generic menu button with an icon that optionally renders a Home Assistant Template. +// class HomeAssistantMenuItem extends WatchUi.IconMenuItem { private var mTemplate as Lang.String or Null; + //! Class Constructor + //! + //! @param label Menu item label + //! @param template Menu item template + //! @param options Menu item options to be passed on. + // function initialize( label as Lang.String or Lang.Symbol, template as Lang.String, @@ -43,14 +46,27 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem { mTemplate = template; } + //! Does this menu item use a template? + //! + //! @return True if the menu has a defined template else false. + // function hasTemplate() as Lang.Boolean { return mTemplate != null; } - function buildTemplate() as Lang.String or Null { + //! Return the menu item's template. + //! + //! @return A string with the menu item's template definition. + // + function getTemplate() as Lang.String or Null { return mTemplate; } + //! 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) { setSubLabel($.Rez.Strings.Empty); @@ -76,4 +92,4 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem { WatchUi.requestUpdate(); } -} \ No newline at end of file +} diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc index e92ff03..bec8239 100644 --- a/source/HomeAssistantMenuItemFactory.mc +++ b/source/HomeAssistantMenuItemFactory.mc @@ -11,17 +11,14 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 17 November 2023 // -// -// Description: -// -// MenuItems Factory. -// //----------------------------------------------------------------------------------- using Toybox.Application; using Toybox.Lang; using Toybox.WatchUi; +//! MenuItems Factory class. +// class HomeAssistantMenuItemFactory { private var mMenuItemOptions as Lang.Dictionary; private var mTapTypeIcon as WatchUi.Bitmap; @@ -31,6 +28,8 @@ class HomeAssistantMenuItemFactory { private static var instance; + //! Class Constructor + // private function initialize() { mMenuItemOptions = { :alignment => Settings.getMenuAlignment() @@ -57,6 +56,8 @@ class HomeAssistantMenuItemFactory { mHomeAssistantService = new HomeAssistantService(); } + //! Create the one and only instance of this class. + // static function create() as HomeAssistantMenuItemFactory { if (instance == null) { instance = new HomeAssistantMenuItemFactory(); @@ -64,6 +65,14 @@ class HomeAssistantMenuItemFactory { return instance; } + //! Toggle menu item. + //! + //! @param label Menu item label. + //! @param entity_id Home Assistant Entity ID (optional) + //! @param template Template for Home Assistant to render (optional) + //! @param confirm Should this menu item selection be confirmed? + //! @param pin Should this menu item selection request the security PIN? + // function toggle( label as Lang.String or Lang.Symbol, entity_id as Lang.String or Null, @@ -81,20 +90,30 @@ class HomeAssistantMenuItemFactory { ); } + //! Tap menu item. + //! + //! @param label Menu item label. + //! @param entity_id Home Assistant Entity ID (optional) + //! @param template Template for Home Assistant to render (optional) + //! @param service Template for Home Assistant to render (optional) + //! @param confirm Should this menu item selection be confirmed? + //! @param pin Should this menu item selection request the security PIN? + //! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field. + // function tap( - label as Lang.String or Lang.Symbol, - entity as Lang.String or Null, - template as Lang.String or Null, - service as Lang.String or Null, - confirm as Lang.Boolean, - pin as Lang.Boolean, - data as Lang.Dictionary or Null + label as Lang.String or Lang.Symbol, + entity_id as Lang.String or Null, + template as Lang.String or Null, + service as Lang.String or Null, + confirm as Lang.Boolean, + pin as Lang.Boolean, + data as Lang.Dictionary or Null ) as WatchUi.MenuItem { - if (entity != null) { + if (entity_id != null) { if (data == null) { - data = { "entity_id" => entity }; + data = { "entity_id" => entity_id }; } else { - data.put("entity_id", entity); + data.put("entity_id", entity_id); } } if (service != null) { @@ -124,6 +143,11 @@ class HomeAssistantMenuItemFactory { } } + //! Group menu item. + //! + //! @param definition Items array from the JSON that defines this sub menu. + //! @param template Template for Home Assistant to render (optional) + // function group( definition as Lang.Dictionary, template as Lang.String or Null diff --git a/source/HomeAssistantPinConfirmation.mc b/source/HomeAssistantPinConfirmation.mc index 7c519bf..436dd39 100644 --- a/source/HomeAssistantPinConfirmation.mc +++ b/source/HomeAssistantPinConfirmation.mc @@ -11,25 +11,28 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Pin Confirmation dialog and logic. -// //----------------------------------------------------------------------------------- -import Toybox.Graphics; -import Toybox.Lang; -import Toybox.WatchUi; -import Toybox.Timer; -import Toybox.Attention; -import Toybox.Time; +using Toybox.Graphics; +using Toybox.Lang; +using Toybox.WatchUi; +using Toybox.Timer; +using Toybox.Attention; +using Toybox.Time; +//! Pin digit used for number 0..9 +// class PinDigit extends WatchUi.Selectable { - private var mDigit as Number; + private var mDigit as Lang.Number; - function initialize(digit as Number, stepX as Number, stepY as Number) { + //! Class Constructor + //! + //! @param digit The digit this instance of the class represents and to display. + //! @param stepX Horizontal spacing. + //! @param stepY Vertical spacing. + // + function initialize(digit as Lang.Number, stepX as Lang.Number, stepY as Lang.Number) { var marginX = stepX * 0.05; // 5% margin on all sides var marginY = stepY * 0.05; var x = (digit == 0) ? stepX : stepX * ((digit+2) % 3); // layout '0' in 2nd col, others ltr in 3 columns @@ -63,24 +66,40 @@ class PinDigit extends WatchUi.Selectable { }); mDigit = digit; - } - function getDigit() as Number { + //! Return the digit 0..9 represented by this button + // + function getDigit() as Lang.Number { return mDigit; } + //! Customised drawing of a PIN digit's button. + // class PinDigitButton extends WatchUi.Drawable { - private var mText as Number; - private var mTouched as Boolean = false; + private var mText as Lang.Number; + private var mTouched as Lang.Boolean = false; + //! Class Constructor + //! + //! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.
+ //! {
+ //!   :label as Lang.Number, // The digit 0..9 to display
+ //!   :touched as Lang.Boolean, // Should the digit be filled to indicate it has been pressed?
+ //!   + those required by `Drawable.initialize()`
+ //! } + // function initialize(options) { Drawable.initialize(options); mText = options.get(:label); mTouched = options.get(:touched); } - function draw(dc) { + //! Draw the PIN digit button. + //! + //! @param dc Device context + // + function draw(dc as Graphics.Dc) { if (mTouched) { dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE); } else { @@ -98,17 +117,27 @@ class PinDigit extends WatchUi.Selectable { } + +//! Pin Confirmation dialog and logic. +// class HomeAssistantPinConfirmationView extends WatchUi.View { - static const MARGIN_X = 20; // margin on left & right side of screen (overall prettier and works better on round displays) + //! Margin on left & right side of screen (overall prettier and works better on round displays) + static const MARGIN_X = 20; + //! Indicates how many digits have been entered so far. + var mPinMask as Lang.String = ""; - var mPinMask as String = ""; - + //! Class Constructor + // function initialize() { View.initialize(); } - function onLayout(dc as Dc) as Void { + //! Construct the view. + //! + //! @param dc Device context + // + function onLayout(dc as Graphics.Dc) as Void { var stepX = (dc.getWidth() - MARGIN_X * 2) / 3; // three columns var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry) var digits = []; @@ -119,7 +148,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View { setLayout(digits); } - function onUpdate(dc as Dc) as Void { + //! Update the view. + //! + //! @param dc Device context + // + function onUpdate(dc as Graphics.Dc) as Void { View.onUpdate(dc); if (mPinMask.length() != 0) { dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); @@ -127,7 +160,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View { } } - function updatePinMask(length as Number) { + //! Update the PIN mask displayed. + //! + //! @param length Number of `*` characters to use for the mask string. + // + function updatePinMask(length as Lang.Number) { mPinMask = ""; for (var i=0; i; - private var mLockedUntil as Number or Null; + private var mFailures as Lang.Array; + private var mLockedUntil as Lang.Number or Null; + //! Class Constructor + // function initialize() { // System.println("PinFailures initialize() Initializing PIN failures from storage"); var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES); @@ -256,6 +326,8 @@ class PinFailures { mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED); } + //! Record a PIN entry failure. If too many have occurred lock the application. + // function addFailure() { mFailures.add(Time.now().value()); // System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded"); @@ -268,7 +340,7 @@ class PinFailures { mFailures = mFailures.slice(1, null); } else { mFailures = []; - mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Gregorian.SECONDS_PER_MINUTE)).value(); + mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Time.Gregorian.SECONDS_PER_MINUTE)).value(); Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil); // System.println("PinFailures addFailure() Locked until " + mLockedUntil); } @@ -276,6 +348,9 @@ class PinFailures { Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures); } + //! Clear the record of previous PIN entry failures, e.g. because the correct PIN has now been entered + //! within tolerance. + // function reset() { // System.println("PinFailures reset() Resetting failures"); mFailures = []; @@ -284,11 +359,18 @@ class PinFailures { Application.Storage.deleteValue(STORAGE_KEY_LOCKED); } - function getLockedUntilSeconds() as Number { + //! Retrieve the remaining time the application must be locked out for. + // + function getLockedUntilSeconds() as Lang.Number { return new Time.Moment(mLockedUntil).subtract(Time.now()).value(); } - function isLocked() as Boolean { + //! Is the application currently locked out? If the application is no longer locked out, then clear the + //! stored values used to determine this state. + //! + //! @return Boolean indicating if the application is currently locked out. + // + function isLocked() as Lang.Boolean { if (mLockedUntil == null) { return false; } diff --git a/source/HomeAssistantService.mc b/source/HomeAssistantService.mc index ba8e0d7..3cfc3b9 100644 --- a/source/HomeAssistantService.mc +++ b/source/HomeAssistantService.mc @@ -11,11 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023 // -// -// Description: -// -// Calling a Home Assistant Service. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -23,10 +18,14 @@ using Toybox.WatchUi; using Toybox.Graphics; using Toybox.Application.Properties; +//! Calling a Home Assistant Service. +// class HomeAssistantService { private var mHasToast as Lang.Boolean = false; private var mHasVibrate as Lang.Boolean = false; + //! Class Constructor + // function initialize() { if (WatchUi has :showToast) { mHasToast = true; @@ -36,7 +35,11 @@ class HomeAssistantService { } } - // Callback function after completing the POST request to call a service. + //! Callback function after completing the POST request to call a service. + //! + //! @param responseCode Response code. + //! @param data Response data. + //! @param context An `entity_id` supplied in the GET request `options` `Lang.Dictionary` `context` field. // function onReturnCall( responseCode as Lang.Number, @@ -107,6 +110,11 @@ class HomeAssistantService { } } + //! Invoke a service call for a menu item. + //! + //! @param service The Home Assistant service to be run, e.g. from the JSON `service` field. + //! @param data Data to be supplied to the service call. + // function call( service as Lang.String, data as Lang.Dictionary or Null diff --git a/source/HomeAssistantTapMenuItem.mc b/source/HomeAssistantTapMenuItem.mc index 9e89e78..0f7a2c1 100644 --- a/source/HomeAssistantTapMenuItem.mc +++ b/source/HomeAssistantTapMenuItem.mc @@ -11,17 +11,14 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Menu button that triggers a service. -// //----------------------------------------------------------------------------------- using Toybox.Lang; using Toybox.WatchUi; using Toybox.Graphics; +//! Menu button that triggers a service. +// class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { private var mHomeAssistantService as HomeAssistantService; private var mService as Lang.String or Null; @@ -29,6 +26,19 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { private var mPin as Lang.Boolean; private var mData as Lang.Dictionary or Null; + //! Class Constructor + //! + //! @param label Menu item label. + //! @param template Menu item template. + //! @param service Menu item service. + //! @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 data Data to supply to the service call. + //! @param icon Icon to use for the menu item. + //! @param options Menu item options to be passed on. + //! @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, template as Lang.String, @@ -62,6 +72,8 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { mData = data; } + //! Call a Home Assistant service only after checks have been done for confirmation or PIN entry. + // function callService() as Void { var hasTouchScreen = System.getDeviceSettings().isTouchScreen; if (mPin && hasTouchScreen) { @@ -85,7 +97,10 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { } } - // NB. Parameter 'b' is ignored + //! 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, mData); diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index cff6c90..13cd2d1 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -11,11 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Light or switch toggle button that calls the API to maintain the up to date state. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -24,6 +19,8 @@ using Toybox.Graphics; using Toybox.Application.Properties; using Toybox.Timer; +//! Light or switch toggle menu button that calls the API to maintain the up to date state. +// class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { private var mConfirm as Lang.Boolean; private var mPin as Lang.Boolean; @@ -31,6 +28,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { private var mTemplate as Lang.String; private var mHasVibrate as Lang.Boolean = false; + //! Class Constructor + //! + //! @param label Menu item label. + //! @param template Menu item template. + //! @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 data Data to supply to the service call. + //! @param options Menu item options to be passed on. + // function initialize( label as Lang.String or Lang.Symbol, template as Lang.String, @@ -58,6 +64,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { mTemplate = template; } + //! Set the state of a toggle menu item. + // private function setUiToggle(state as Null or Lang.String) as Void { if (state != null) { if (state.equals("on") && !isEnabled()) { @@ -68,13 +76,26 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } } - function buildTemplate() as Lang.String or Null { + //! Return the menu item's template. + //! + //! @return A string with the menu item's template definition. + // + function getTemplate() as Lang.String or Null { return mTemplate; } - function buildToggleTemplate() as Lang.String or Null { + + //! Return a toggle menu item's state template. + //! + //! @return A string with the menu item's template definition. + // + function getToggleTemplate() as Lang.String or Null { return "{{states('" + mData.get("entity_id") + "')}}"; } + //! Update the menu item's label from a recent GET request. + //! + //! @param data This should be a string, but the way the GET response is parsed, it can also be a number. + // function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void { if (data == null) { setSubLabel(null); @@ -100,6 +121,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { WatchUi.requestUpdate(); } + //! Update the menu item's toggle state from a recent GET request. + //! + //! @param data This should be a string of either "on" or "off". + // function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void { if (data == null) { setUiToggle("off"); @@ -126,9 +151,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { WatchUi.requestUpdate(); } - // Callback function after completing the POST request to set the status. + //! Callback function after completing the POST request to set the status. + //! + //! @param responseCode Response code. + //! @param data Response data. // - function onReturnSetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + function onReturnSetState( + responseCode as Lang.Number, + data as Null or Lang.Dictionary or Lang.String + ) as Void { // System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Code: " + responseCode); // System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Data: " + data); @@ -183,6 +214,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { getApp().setApiStatus(status); } + //! Set the state of the toggle menu item. + //! + //! @param s Boolean indicating the desired state of the toggle switch. + // function setState(s as Lang.Boolean) as Void { // Toggle the UI back, we'll wait for confirmation from the Home Assistant setEnabled(!isEnabled()); @@ -228,6 +263,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } } + //! Call a Home Assistant service only after checks have been done for confirmation or PIN entry. + // function callService(b as Lang.Boolean) as Void { var hasTouchScreen = System.getDeviceSettings().isTouchScreen; if (mPin && hasTouchScreen) { @@ -251,6 +288,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { } } + //! Callback function to toggle state of this item after (optional) confirmation. + //! + //! @param b Desired toggle button state. + // function onConfirm(b as Lang.Boolean) as Void { setState(b); } diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc index f894432..1c1c888 100644 --- a/source/HomeAssistantView.mc +++ b/source/HomeAssistantView.mc @@ -11,11 +11,6 @@ // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // -// -// Description: -// -// Home Assistant menu construction. -// //----------------------------------------------------------------------------------- using Toybox.Application; @@ -24,8 +19,12 @@ 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 { @@ -75,7 +74,12 @@ class HomeAssistantView extends WatchUi.Menu2 { } } - // Lang.Array.addAll() fails structural type checking without including "Null" in the return type + //! 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; @@ -102,26 +106,35 @@ class HomeAssistantView extends WatchUi.Menu2 { 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. + //! 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 {} } -// -// Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/ +//! 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(); } + //! Back button event + // function onBack() { mTimer.reset(); @@ -135,16 +148,22 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { WatchUi.popView(WatchUi.SLIDE_RIGHT); } - // Only for CheckboxMenu + //! Only for CheckboxMenu + // function onDone() { mTimer.reset(); } - // Only for CustomMenu + //! 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) { @@ -164,7 +183,8 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { } } - // Only for CustomMenu + //! Only for CustomMenu + // function onTitle() { mTimer.reset(); } diff --git a/source/QuitTimer.mc b/source/QuitTimer.mc index a15efb0..7accd8d 100644 --- a/source/QuitTimer.mc +++ b/source/QuitTimer.mc @@ -11,11 +11,6 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// Quit the application after a period of inactivity in order to save the battery. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -23,18 +18,27 @@ using Toybox.Timer; using Toybox.Application.Properties; using Toybox.WatchUi; +//! Quit the application after a period of inactivity in order to save the battery. +//! class QuitTimer extends Timer.Timer { + //! Class Constructor + // function initialize() { Timer.Timer.initialize(); } + //! Can't see how to make a method object from `System.exit()` without this layer of + //! indirection. I assume this is because `System` is a static class. + // function exitApp() as Void { // System.println("QuitTimer exitApp(): Exiting"); // This will exit the system cleanly from any point within an app. System.exit(); } + //! Kick off the quit timer. + // function begin() { var api_timeout = Settings.getAppTimeout(); // ms if (api_timeout > 0) { @@ -42,6 +46,8 @@ class QuitTimer extends Timer.Timer { } } + //! Reset the quit timer. + // function reset() { // System.println("QuitTimer reset(): Restarted quit timer"); stop(); diff --git a/source/ScalableView.mc b/source/ScalableView.mc index 5d1a4bf..f381930 100644 --- a/source/ScalableView.mc +++ b/source/ScalableView.mc @@ -11,35 +11,36 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// A view with added methods to scale from percentages of scrren size to pixels. -// //----------------------------------------------------------------------------------- using Toybox.Lang; using Toybox.WatchUi; using Toybox.Math; +//! A view that provides a common method 'pixelsForScreen' to make Views easier to layout on different +//! sized watch screens. +// class ScalableView extends WatchUi.View { + //! Retain the local screen width for efficiency private var mScreenWidth; + //! Class Constructor + // function initialize() { View.initialize(); mScreenWidth = System.getDeviceSettings().screenWidth; } - // Convert a fraction expressed as a percentage (%) to a number of pixels for the - // screen's dimensions. - // - // Parameters: - // * dc - Device context - // * pc - Percentage (%) expressed as a number in the range 0.0..100.0 - // - // Uses screen width rather than screen height as rectangular screens tend to have - // height > width. - // + //! Convert a fraction expressed as a percentage (%) to a number of pixels for the + //! screen's dimensions. + //! + //! Uses screen width rather than screen height as rectangular screens tend to have + //! height > width. + //! + //! @param pc Percentage (%) expressed as a number in the range 0.0..100.0 + //! + //! @return Number of pixels for the screen's dimensions for a fraction expressed as a percentage (%). + //! function pixelsForScreen(pc as Lang.Float) as Lang.Number { return Math.round(pc * mScreenWidth) / 100; } diff --git a/source/Settings.mc b/source/Settings.mc index 283c276..658c9a8 100644 --- a/source/Settings.mc +++ b/source/Settings.mc @@ -11,16 +11,6 @@ // // P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 23 November 2023 // -// -// Description: -// -// Home Assistant settings. -// -// WARNING! -// -// Careful putting ErrorView.show() calls in here. They need to be guarded so that -// they do not get called when only displaying the glance view. -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -31,6 +21,11 @@ using Toybox.System; using Toybox.Background; using Toybox.Time; +//! Home Assistant settings. +//! +//! WARNING! Careful putting ErrorView.show() calls in here. They need to be +//! guarded so that they do not get called when only displaying the glance view. +// (:glance, :background) class Settings { private static var mApiKey as Lang.String = ""; @@ -40,19 +35,24 @@ class Settings { private static var mCacheConfig as Lang.Boolean = false; private static var mClearCache as Lang.Boolean = false; private static var mVibrate as Lang.Boolean = false; - private static var mAppTimeout as Lang.Number = 0; // seconds - private static var mPollDelay as Lang.Number = 0; // seconds - private static var mConfirmTimeout as Lang.Number = 3; // seconds + //! seconds + private static var mAppTimeout as Lang.Number = 0; + //! seconds + private static var mPollDelay as Lang.Number = 0; + //! seconds + private static var mConfirmTimeout as Lang.Number = 3; private static var mPin as Lang.String or Null = "0000"; private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT; private static var mIsSensorsLevelEnabled as Lang.Boolean = false; - private static var mBatteryRefreshRate as Lang.Number = 15; // minutes + //! minutes + private static var mBatteryRefreshRate as Lang.Number = 15; private static var mIsApp as Lang.Boolean = false; private static var mHasService as Lang.Boolean = false; - // Must keep the object so it doesn't get garbage collected. + //! Must keep the object so it doesn't get garbage collected. private static var mWebhookManager as WebhookManager or Null; - // Called on application start and then whenever the settings are changed. + //! Called on application start and then whenever the settings are changed. + // static function update() { mIsApp = getApp().getIsApp(); mApiKey = Properties.getValue("api_key"); @@ -71,6 +71,8 @@ class Settings { mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate"); } + //! A webhook is required for non-privileged API calls. + // static function webhook() { if (System has :ServiceDelegate) { mHasService = true; @@ -116,65 +118,123 @@ class Settings { // } } + //! Get the API key supplied as part of the Settings. + //! + //! @return The API Key + // static function getApiKey() as Lang.String { return mApiKey; } + //! Get the Webhook ID supplied as part of the Settings. + //! + //! @return The Webhook ID + // static function getWebhookId() as Lang.String { return mWebhookId; } + //! Set the Webhook ID supplied as part of the Settings. + //! + //! @param webhookId The Webhook ID value to be saved. + // static function setWebhookId(webhookId as Lang.String) { mWebhookId = webhookId; Properties.setValue("webhook_id", mWebhookId); } + //! Delete the Webhook ID saved as part of the Settings. + // static function unsetWebhookId() { mWebhookId = ""; Properties.setValue("webhook_id", mWebhookId); } + //! Get the API URL supplied as part of the Settings. + //! + //! @return The API URL + // static function getApiUrl() as Lang.String { return mApiUrl; } + //! Get the menu configuration URL supplied as part of the Settings. + //! + //! @return The menu configuration URL + // static function getConfigUrl() as Lang.String { return mConfigUrl; } + //! Get the menu cache Boolean option supplied as part of the Settings. + //! + //! @return Boolean for whether the menu should be cached to save application + //! start up time. + // static function getCacheConfig() as Lang.Boolean { return mCacheConfig; } + //! Get the clear cache Boolean option supplied as part of the Settings. + //! + //! @return Boolean for whether the cache should be cleared next time the + //! application is started, forcing a menu refresh. + // static function getClearCache() as Lang.Boolean { return mClearCache; } + //! Unset the clear cache Boolean option supplied as part of the Settings. + // static function unsetClearCache() { mClearCache = false; Properties.setValue("clear_cache", mClearCache); } + //! Get the vibration Boolean option supplied as part of the Settings. + //! + //! @return Boolean for whether vibration is enabled. + // static function getVibrate() as Lang.Boolean { return mVibrate; } + //! Get the application timeout value supplied as part of the Settings. + //! + //! @return The application timeout in milliseconds. + // static function getAppTimeout() as Lang.Number { return mAppTimeout * 1000; // Convert to milliseconds } + //! Get the application API polling interval supplied as part of the Settings. + //! + //! @return The application API polling interval in milliseconds. + // static function getPollDelay() as Lang.Number { return mPollDelay * 1000; // Convert to milliseconds } + //! Get the menu item confirmation delay supplied as part of the Settings. + //! + //! @return The menu item confirmation delay in milliseconds. + // static function getConfirmTimeout() as Lang.Number { return mConfirmTimeout * 1000; // Convert to milliseconds } + //! Get the menu item security PIN supplied as part of the Settings. + //! + //! @return The menu item security PIN. + // static function getPin() as Lang.String or Null { return mPin; } + //! Check the user selected PIN confirms to 4 digits as a string. + //! + //! @return The validated 4 digit string. + // private static function validatePin() as Lang.String or Null { var pin = Properties.getValue("pin"); if (pin.toNumber() == null || pin.length() != 4) { @@ -183,14 +243,24 @@ class Settings { return pin; } + //! Get the menu item alignment as part of the Settings. + //! + //! @return The menu item alignment. + // static function getMenuAlignment() as Lang.Number { return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT } + //! Is logging of the watch sensors enabled? E.g. battery, activity etc. + //! + //! @return Boolean for whether logging of the watch sensors is enabled. + // static function isSensorsLevelEnabled() as Lang.Boolean { return mIsSensorsLevelEnabled; } + //! Disable logging of the watch's sensors. + // static function unsetIsSensorsLevelEnabled() { mIsSensorsLevelEnabled = false; Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled); diff --git a/source/WebLog.mc b/source/WebLog.mc index 05dec93..53858f9 100644 --- a/source/WebLog.mc +++ b/source/WebLog.mc @@ -11,88 +11,99 @@ // // J D Abbey & P A Abbey, 28 December 2022 // -// -// Description: -// -// WebLog provides a logging and hence debugging aid for when the application is -// deployed to the watch. This is only used for development and use of it must not -// persist into a deployed version. It uses a string buffer to group log entries into -// larger submissions in order to prevent overflow of the blue tooth stack. -// //----------------------------------------------------------------------------------- -// -// Usage: -// wl = new WebLog(); -// wl.clear(); -// wl.println("Debug Message"); -// wl.flush(); -// -// https://domain.name/path/log.php -// -// -// -// Logs published to: https://domain.name/path/log -// -// https://domain.name/path/log_clear.php -// -// using Toybox.Communications; using Toybox.Lang; using Toybox.System; +//! WebLog provides a logging and hence debugging aid for when the application is +//! deployed to the watch. This is only used for development and use of it must not +//! persist into a deployed version. It uses a string buffer to group log entries into +//! larger submissions in order to prevent overflow of the Bluetooth stack. +//! +//! Usage: +//!
+//!   wl = new WebLog();
+//!   wl.clear();
+//!   wl.println("Debug Message");
+//!   wl.flush();
+//! 
+//! +//! File: https://domain.name/path/log.php +//! +//!
+//! <?php
+//!   $myfile = fopen("log", "a");
+//!   $queries = array();
+//!   parse_str($_SERVER['QUERY_STRING'], $queries);
+//!   fwrite($myfile, $queries['log']);
+//!   print "Success";
+//! ?>
+//! 
+//! +//! Logs published to https://domain.name/path/log. +//! +//! File: https://domain.name/path/log_clear.php +//! +//!
+//! <?php
+//!   $myfile = fopen("log", "w");
+//!   fwrite($myfile, "");
+//!   print "Success";
+//! ?>
+//! 
+// (:glance, :background) class WebLog { - private var callsbuffer = 4 as Lang.Number; + private var callsBuffer = 4 as Lang.Number; private var numCalls = 0 as Lang.Number; private var buffer = "" as Lang.String; - // Set the number of calls to print() before sending the buffer to the online - // logger. + //! Set the number of calls to print() before sending the buffer to the online + //! logger. + //! + //! @param l The number of log calls to buffer before writing to the online service. // function setCallsBuffer(l as Lang.Number) { - callsbuffer = l; + callsBuffer = l; } - // Get the number of calls to print() before sending the buffer to the online - // logger. + //! Get the number of calls to print() before sending the buffer to the online + //! logger. + //! + //! @return The number of log calls to buffer before writing to the online service. // function getCallsBuffer() as Lang.Number { - return callsbuffer; + return callsBuffer; } - // Create a debug log over the Internet to keep track of the watch's runtime - // execution. + //! Create a debug log over the Internet to keep track of the watch's runtime + //! execution. + //! + //! @param str The string to log. // function print(str as Lang.String) { var myTime = System.getClockTime(); buffer += myTime.hour.format("%02d") + ":" + myTime.min.format("%02d") + ":" + myTime.sec.format("%02d") + " " + str; numCalls++; // System.println("WebLog print() str = " + str); - if (numCalls >= callsbuffer) { + if (numCalls >= callsBuffer) { doPrint(); } } - // Create a debug log over the Internet to keep track of the watch's runtime - // execution. Add a new line character to the end. + //! Create a debug log over the Internet to keep track of the watch's runtime + //! execution. Add a new line character to the end. + //! + //! @param str The string to log. // function println(str as Lang.String) { print(str + "\n"); } - // Flush the current buffer to the online logger even if it has not reach the - // submission level set by 'callsbuffer'. + //! Flush the current buffer to the online logger even if it has not reach the + //! submission level set by 'callsBuffer'. // function flush() { // System.println("WebLog flush()"); @@ -101,7 +112,7 @@ class WebLog { } } - // Perform the submission to the online logger. + //! Perform the submission to the online logger. // function doPrint() { // System.println("WebLog doPrint()"); @@ -122,8 +133,8 @@ class WebLog { buffer = ""; } - // Clear the debug log over the Internet to start a new track of the watch's runtime - // execution. + //! Clear the debug log over the Internet to start a new track of the watch's runtime + //! execution. // function clear() { // System.println("WebLog clear()"); @@ -143,7 +154,10 @@ class WebLog { buffer = ""; } - // Callback function to print the outcome of a doPrint() method. + //! Callback function to print the outcome of a doPrint() method. Typically used for debugging this class. + //! + //! @param responseCode Response code. + //! @param data Response data. // function onLog(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { // if (responseCode != 200) { @@ -153,7 +167,10 @@ class WebLog { // } } - // Callback function to print the outcome of a clear() method. + // Callback function to print the outcome of a clear() method. Typically used for debugging this class. + //! + //! @param responseCode Response code. + //! @param data Response data. // function onClear(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { // if (responseCode != 200) { diff --git a/source/WebhookManager.mc b/source/WebhookManager.mc index c44ccec..07e0cbb 100644 --- a/source/WebhookManager.mc +++ b/source/WebhookManager.mc @@ -11,14 +11,6 @@ // // P A Abbey & J D Abbey, 10 January 2024 // -// -// Description: -// -// Home Assistant Webhook creation. -// -// Reference: -// * https://developers.home-assistant.io/docs/api/native-app-integration -// //----------------------------------------------------------------------------------- using Toybox.Lang; @@ -26,10 +18,24 @@ using Toybox.Communications; using Toybox.System; using Toybox.WatchUi; -// Can use push view so must never be run in a glance context +//! Home Assistant Webhook creation. +//! +//! NB. Because we can use push view (E.g. `ErrorView.show()`) this class must never +//! be run in a glance context. +//! +//! Reference: https://developers.home-assistant.io/docs/api/native-app-integration +// class WebhookManager { - function onReturnRequestWebhookId(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { + //! Callback for requesting a Webhook ID. + //! + //! @param responseCode Response code + //! @param data Return data + // + function onReturnRequestWebhookId( + responseCode as Lang.Number, + data as Null or Lang.Dictionary or Lang.String + ) as Void { switch (responseCode) { case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_CONNECTION_UNAVAILABLE: @@ -84,6 +90,8 @@ class WebhookManager { } } + //! Request a Webhook ID from Home Assistant for use in this application. + // function requestWebhookId() { var deviceSettings = System.getDeviceSettings(); // System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier); @@ -115,7 +123,18 @@ class WebhookManager { ); } - function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, sensors as Lang.Array) as Void { + //! Callback function for the POST request to register the watch's sensors on the Home Assistant instance. + //! + //! @param responseCode Response code. + //! @param data Response data. + //! @param sensors The remaining sensors to be processed. The list of sensors is iterated through + //! until empty. Each POST request creating one sensor on the local Home Assistant. + // + function onReturnRegisterWebhookSensor( + responseCode as Lang.Number, + data as Null or Lang.Dictionary or Lang.String, + sensors as Lang.Array + ) as Void { switch (responseCode) { case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_CONNECTION_UNAVAILABLE: @@ -194,7 +213,11 @@ class WebhookManager { } } - function registerWebhookSensor(sensors as Lang.Array) { + //! Local method to send the POST request to register a number of sensors. + //! + //! @param sensors An array of sensors, e.g. As created by `registerWebhookSensors()`. + // + private function registerWebhookSensor(sensors as Lang.Array) { var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(); // System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString()); // System.println("WebhookManager registerWebhookSensor(): URL=" + url); @@ -217,6 +240,8 @@ class WebhookManager { ); } + //! Request the creation of all the supported watch sensors on the Home Assistant instance. + // function registerWebhookSensors() { var heartRate = Activity.getActivityInfo().currentHeartRate;