Added correctly formatted code comments (#246)

The newer SDK support tooltips to show the function prototype and help
text, so best to make good use of it.

I'm not expecting this to be 100% on the first iteration.
This commit is contained in:
__JosephAbbey
2025-07-05 13:58:35 +01:00
committed by GitHub
22 changed files with 831 additions and 316 deletions

View File

@ -4,5 +4,9 @@
"path": "." "path": "."
} }
], ],
"settings": {} "settings": {
"cSpell.words": [
"Initialiser"
]
}
} }

View File

@ -11,15 +11,6 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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; using Toybox.Lang;
@ -27,6 +18,12 @@ using Toybox.Graphics;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Timer; 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 { class Alert extends WatchUi.View {
private static const scRadius = 10; private static const scRadius = 10;
private var mTimer as Timer.Timer; private var mTimer as Timer.Timer;
@ -36,6 +33,16 @@ class Alert extends WatchUi.View {
private var mFgcolor as Graphics.ColorType; private var mFgcolor as Graphics.ColorType;
private var mBgcolor as Graphics.ColorType; private var mBgcolor as Graphics.ColorType;
//! Class Constructor
//! @param params A dictionary object as follows:<br>
//! &lbrace;<br>
//! &emsp; :timeout as Lang.Number, // Timeout in millseconds<br>
//! &emsp; :font as Graphics.FontType, // Text font size<br>
//! &emsp; :text as Lang.String, // Text to display<br>
//! &emsp; :fgcolor as Graphics.ColorType, // Foreground Colour<br>
//! &emsp; :bgcolor as Graphics.ColorType // Background Colour<br>
//! &rbrace;
//
function initialize(params as Lang.Dictionary) { function initialize(params as Lang.Dictionary) {
View.initialize(); View.initialize();
@ -67,14 +74,22 @@ class Alert extends WatchUi.View {
mTimer = new Timer.Timer(); mTimer = new Timer.Timer();
} }
//! Setup a timer to dismiss the alert.
//
function onShow() { function onShow() {
mTimer.start(method(:dismiss), mTimeout, false); mTimer.start(method(:dismiss), mTimeout, false);
} }
//! Prematurely stop the timer.
//
function onHide() { function onHide() {
mTimer.stop(); mTimer.stop();
} }
//! Draw the Alert view.
//!
//! @param dc Device context
//
function onUpdate(dc as Graphics.Dc) { function onUpdate(dc as Graphics.Dc) {
var tWidth = dc.getTextWidthInPixels(mText, mFont); var tWidth = dc.getTextWidthInPixels(mText, mFont);
var tHeight = dc.getFontHeight(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); 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 { function dismiss() as Void {
WatchUi.popView(SLIDE_IMMEDIATE); 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); WatchUi.pushView(self, new AlertDelegate(self), transition);
} }
} }
//! Input Delegate for the Alert view.
//
class AlertDelegate extends WatchUi.InputDelegate { 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(); InputDelegate.initialize();
mView = view; 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(); mView.dismiss();
getApp().getQuitTimer().reset(); getApp().getQuitTimer().reset();
return true; 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(); mView.dismiss();
getApp().getQuitTimer().reset(); getApp().getQuitTimer().reset();
return true; return true;

View File

@ -11,12 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // 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; using Toybox.Lang;
@ -25,20 +19,43 @@ using Toybox.Background;
using Toybox.System; using Toybox.System;
using Toybox.Activity; using Toybox.Activity;
//! The background service delegate reports the Garmin watch's various status values
//! back to the Home Assistant instance.
//
(:background) (:background)
class BackgroundServiceDelegate extends System.ServiceDelegate { class BackgroundServiceDelegate extends System.ServiceDelegate {
//! Class Constructor
//
function initialize() { function initialize() {
ServiceDelegate.initialize(); ServiceDelegate.initialize();
} }
function onReturnBatteryUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { //! Callback function for doUpdate().
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Code: " + responseCode); //!
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Data: " + data); //! @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); 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.<br>
//! &lbrace;<br>
//! &emsp; :sport as Activity.Sport<br>
//! &emsp; :subSport as Activity.SubSport<br>
//! &rbrace;
//
function onActivityCompleted(
activity as {
:sport as Activity.Sport,
:subSport as Activity.SubSport
}
) as Void {
if (!System.getDeviceSettings().phoneConnected) { if (!System.getDeviceSettings().phoneConnected) {
// System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call."); // System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call.");
} else if (!System.getDeviceSettings().connectionAvailable) { } 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 { function onTemporalEvent() as Void {
if (!System.getDeviceSettings().phoneConnected) { if (!System.getDeviceSettings().phoneConnected) {
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call."); // 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."); // System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
var position = Position.getInfo(); var position = Position.getInfo();
// System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees()); // System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees());
@ -136,7 +163,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
}, },
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
}, },
method(:onReturnBatteryUpdate) method(:onReturnDoUpdate)
); );
} }
var activityInfo = ActivityMonitor.getInfo(); var activityInfo = ActivityMonitor.getInfo();
@ -222,7 +249,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
}, },
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
}, },
method(:onReturnBatteryUpdate) method(:onReturnDoUpdate)
); );
} }

View File

@ -11,15 +11,12 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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) (:glance)
class ClientId { class ClientId {
static const webLogUrl = "https://..."; static const webLogUrl = "https://...";

View File

@ -11,22 +11,6 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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<WatchUi.Views or WatchUi.InputDelegates>
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Graphics; using Toybox.Graphics;
@ -35,6 +19,19 @@ using Toybox.WatchUi;
using Toybox.Communications; using Toybox.Communications;
using Toybox.Timer; 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<WatchUi.Views or WatchUi.InputDelegates>`
//
class ErrorView extends ScalableView { class ErrorView extends ScalableView {
private static const scErrorIconMargin as Lang.Float = 7f; private static const scErrorIconMargin as Lang.Float = 7f;
private var mText as Lang.String = ""; private var mText as Lang.String = "";
@ -48,9 +45,11 @@ class ErrorView extends ScalableView {
private static var instance; private static var instance;
private static var mShown as Lang.Boolean = false; private static var mShown as Lang.Boolean = false;
//! Class Constructor
//
function initialize() { function initialize() {
ScalableView.initialize(); ScalableView.initialize();
mDelegate = new ErrorDelegate(self); mDelegate = new ErrorDelegate();
// Convert the settings from % of screen size to pixels // Convert the settings from % of screen size to pixels
mErrorIconMargin = pixelsForScreen(scErrorIconMargin); mErrorIconMargin = pixelsForScreen(scErrorIconMargin);
mErrorIcon = Application.loadResource(Rez.Drawables.ErrorIcon) as Graphics.BitmapResource; 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 { function onLayout(dc as Graphics.Dc) as Void {
var w = dc.getWidth(); 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 { function onUpdate(dc as Graphics.Dc) as Void {
var w = dc.getWidth(); var w = dc.getWidth();
if (mAntiAlias) { if (mAntiAlias) {
@ -87,10 +92,18 @@ class ErrorView extends ScalableView {
mTextArea.draw(dc); mTextArea.draw(dc);
} }
//! Get this view's delegate for processing events.
//
function getDelegate() as ErrorDelegate { function getDelegate() as ErrorDelegate {
return mDelegate; 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 ] { static function create(text as Lang.String) as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
if (instance == null) { if (instance == null) {
instance = new ErrorView(); instance = new ErrorView();
@ -102,7 +115,10 @@ class ErrorView extends ScalableView {
return [instance, instance.getDelegate()]; 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 { static function show(text as Lang.String) as Void {
if (!mShown) { if (!mShown) {
create(text); // Ignore returned values 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 { static function unShow() as Void {
if (mShown) { if (mShown) {
WatchUi.popView(WatchUi.SLIDE_DOWN); 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 { function setText(text as Lang.String) as Void {
mText = text; mText = text;
if (mTextArea != null) { if (mTextArea != null) {
@ -137,12 +158,19 @@ class ErrorView extends ScalableView {
} }
//! Delegate for the ErrorView.
//
class ErrorDelegate extends WatchUi.BehaviorDelegate { class ErrorDelegate extends WatchUi.BehaviorDelegate {
function initialize(view as ErrorView) { //! Class Constructor
//!
function initialize() {
WatchUi.BehaviorDelegate.initialize(); WatchUi.BehaviorDelegate.initialize();
} }
//! Process the event to clear the ErrorView.
//
function onBack() as Lang.Boolean { function onBack() as Lang.Boolean {
getApp().getQuitTimer().reset(); getApp().getQuitTimer().reset();
ErrorView.unShow(); ErrorView.unShow();

View File

@ -11,30 +11,38 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// //
//
// Description:
//
// Home Assistant centralised constants.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Lang; using Toybox.Lang;
//! Home Assistant centralised constants.
//
(:glance) (:glance)
class Globals { 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 scAlertTimeout = 2000; // ms
static const scTapTimeout = 1000; // ms
// Time to let the existing HTTP responses get serviced after a //! Time to let the existing HTTP responses get serviced after a
// Communications.NETWORK_RESPONSE_OUT_OF_MEMORY response code. //! `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` response code.
static const scApiBackoff = 1000; // ms 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 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. static const scLowMem = 0.90; // percent as a fraction.
// Constants for PIN confirmation dialog //! Constant for PIN confirmation dialog.<br>
static const scPinMaxFailures = 5; // Maximum number of failed PIN confirmation attemps allwed in ... //! Maximum number of failed PIN confirmation attempts allowed in `scPinMaxFailureMinutes`.
static const scPinMaxFailureMinutes = 2; // ... this number of minutes before PIN confirmation is locked for ... static const scPinMaxFailures = 5;
static const scPinLockTimeMinutes = 10; // ... this number of minutes
//! Constant for PIN confirmation dialog.<br>
//! Period in minutes during which no more than `scPinMaxFailures` PIN attempts are tolerated.
static const scPinMaxFailureMinutes = 2;
//! Constant for PIN confirmation dialog.<br>
//! Lock out time in minutes after a failed PIN entry.
static const scPinLockTimeMinutes = 10;
} }

View File

@ -11,11 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// //
//
// Description:
//
// Application root for GarminHomeAssistant
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Application; using Toybox.Application;
@ -25,6 +20,8 @@ using Toybox.System;
using Toybox.Application.Properties; using Toybox.Application.Properties;
using Toybox.Timer; using Toybox.Timer;
//! Application root for GarminHomeAssistant
//
(:glance, :background) (:glance, :background)
class HomeAssistantApp extends Application.AppBase { class HomeAssistantApp extends Application.AppBase {
private var mApiStatus as Lang.String or Null; 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 mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
private var mTemplates as Lang.Dictionary = {}; private var mTemplates as Lang.Dictionary = {};
//! Class Constructor
//
function initialize() { function initialize() {
AppBase.initialize(); AppBase.initialize();
// ATTENTION when adding stuff into this block: // ATTENTION when adding stuff into this block:
@ -55,7 +54,10 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)". // 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 { function onStart(state as Lang.Dictionary?) as Void {
AppBase.onStart(state); AppBase.onStart(state);
// ATTENTION when adding stuff into this block: // ATTENTION when adding stuff into this block:
@ -71,7 +73,11 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)". // 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 { function onStop(state as Lang.Dictionary?) as Void {
AppBase.onStop(state); AppBase.onStop(state);
// ATTENTION when adding stuff into this block: // ATTENTION when adding stuff into this block:
@ -87,7 +93,10 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)". // 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 ] { function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
mIsApp = true; mIsApp = true;
mQuitTimer = new QuitTimer(); 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) (: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 Code: " + responseCode);
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data); // System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data);
@ -208,8 +223,11 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.requestUpdate(); 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 //! Fetch the menu configuration over HTTPS, which might be locally cached.
// asynchronous and affects how the views are managed. //!
//! @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) (:glance)
function fetchMenuConfig() as Lang.Boolean { function fetchMenuConfig() as Lang.Boolean {
// System.println("Menu URL = " + Settings.getConfigUrl()); // System.println("Menu URL = " + Settings.getConfigUrl());
@ -263,6 +281,10 @@ class HomeAssistantApp extends Application.AppBase {
return false; 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) { private function buildMenu(menu as Lang.Dictionary) {
mHaMenu = new HomeAssistantView(menu, null); mHaMenu = new HomeAssistantView(menu, null);
mQuitTimer.begin(); 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. } // 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() { function startUpdates() {
if (mHaMenu != null and !mUpdating) { if (mHaMenu != null and !mUpdating) {
// Start the continuous update process that continues for as long as the application is running. // 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 Code: " + responseCode);
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data); // System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
@ -353,6 +385,8 @@ class HomeAssistantApp extends Application.AppBase {
setApiStatus(status); setApiStatus(status);
} }
//! Construct the GET request to update all menu items.
//
function updateMenuItems() as Void { function updateMenuItems() as Void {
if (! System.getDeviceSettings().phoneConnected) { if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call."); // System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
@ -368,7 +402,7 @@ class HomeAssistantApp extends Application.AppBase {
mTemplates = {}; mTemplates = {};
for (var i = 0; i < mItemsToUpdate.size(); i++) { for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i]; var item = mItemsToUpdate[i];
var template = item.buildTemplate(); var template = item.getTemplate();
if (template != null) { if (template != null) {
mTemplates.put(i.toString(), { mTemplates.put(i.toString(), {
"template" => template "template" => template
@ -376,7 +410,7 @@ class HomeAssistantApp extends Application.AppBase {
} }
if (item instanceof HomeAssistantToggleMenuItem) { if (item instanceof HomeAssistantToggleMenuItem) {
mTemplates.put(i.toString() + "t", { mTemplates.put(i.toString() + "t", {
"template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate() "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate()
}); });
} }
} }
@ -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) (: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 Code: " + responseCode);
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data); // System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data);
@ -466,6 +506,8 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} }
//! Construct the GET request to test the API status, is it accessible?
//
(:glance) (:glance)
function fetchApiStatus() as Void { function fetchApiStatus() as Void {
// System.println("API URL = " + Settings.getApiUrl()); // 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) { function setApiStatus(s as Lang.String) {
mApiStatus = s; mApiStatus = s;
} }
//! Return the API status result.
//!
//! @return A string describing the API status
//
(:glance) (:glance)
function getApiStatus() as Lang.String { function getApiStatus() as Lang.String {
return mApiStatus; return mApiStatus;
} }
//! Return the Menu status result.
//!
//! @return A string describing the Menu status
//
(:glance) (:glance)
function getMenuStatus() as Lang.String { function getMenuStatus() as Lang.String {
return mMenuStatus; return mMenuStatus;
} }
//! Return the Menu construction status.
//!
//! @return A Boolean indicating if the menu is loaded into the application.
//
function isHomeAssistantMenuLoaded() as Lang.Boolean { function isHomeAssistantMenuLoaded() as Lang.Boolean {
return mHaMenu != null; return mHaMenu != null;
} }
//! Make the menu visible on the watch face.
//
function pushHomeAssistantMenuView() as Void { function pushHomeAssistantMenuView() as Void {
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE); 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 //! Force status updates. Only take action if `Settings.getPollDelay() > 0`. This must be tested
// alternative action if the test fails. //! locally as it is then efficient to take alternative action if the test fails.
//
function forceStatusUpdates() as Void { function forceStatusUpdates() as Void {
// Don't mess with updates unless we are using a timer. // Don't mess with updates unless we are using a timer.
if (Settings.getPollDelay() > 0) { 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 { function getQuitTimer() as QuitTimer {
return mQuitTimer; return mQuitTimer;
} }
//! Return the glance view.
//!
//! @return The glance view
//
function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null { function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
mIsGlance = true; mIsGlance = true;
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String; mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
@ -554,30 +623,38 @@ class HomeAssistantApp extends Application.AppBase {
return [new HomeAssistantGlanceView(self)]; 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 { function updateStatus() as Void {
mGlanceTimer = null; mGlanceTimer = null;
fetchMenuConfig(); fetchMenuConfig();
fetchApiStatus(); fetchApiStatus();
} }
//! Code for when the application settings are updated.
//
function onSettingsChanged() as Void { function onSettingsChanged() as Void {
// System.println("HomeAssistantApp onSettingsChanged()"); // System.println("HomeAssistantApp onSettingsChanged()");
Settings.update(); Settings.update();
} }
// Called each time the Registered Temporal Event is to be invoked. So the object is created each time on request and //! Called each time the Registered Temporal Event is to be invoked. So the object is created each time
// then destroyed on completion (to save resources). //! on request and then destroyed on completion (to save resources).
//
function getServiceDelegate() as [ System.ServiceDelegate ] { function getServiceDelegate() as [ System.ServiceDelegate ] {
return [new BackgroundServiceDelegate()]; 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 { function getIsApp() as Lang.Boolean {
return mIsApp; return mIsApp;
} }
} }
//! Global function to return the application object.
//
(:glance, :background) (:glance, :background)
function getApp() as HomeAssistantApp { function getApp() as HomeAssistantApp {
return Application.getApp() as HomeAssistantApp; return Application.getApp() as HomeAssistantApp;

View File

@ -11,11 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023 // P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
// //
//
// Description:
//
// Calling a Home Assistant confirmation dialogue view.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Lang; using Toybox.Lang;
@ -25,19 +20,27 @@ using Toybox.WatchUi;
using Toybox.Timer; using Toybox.Timer;
using Toybox.Application.Properties; using Toybox.Application.Properties;
//! Calling a Home Assistant confirmation dialogue view.
//
class HomeAssistantConfirmation extends WatchUi.Confirmation { class HomeAssistantConfirmation extends WatchUi.Confirmation {
//! Class Constructor
//
function initialize() { function initialize() {
WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String); WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String);
} }
} }
//! Delegate to respond to the confirmation request.
//
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate { class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
private var mConfirmMethod as Method(state as Lang.Boolean) as Void; private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
private var mTimer as Timer.Timer or Null; private var mTimer as Timer.Timer or Null;
private var mState as Lang.Boolean; private var mState as Lang.Boolean;
//! Class Constructor
//
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) { function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) {
WatchUi.ConfirmationDelegate.initialize(); WatchUi.ConfirmationDelegate.initialize();
mConfirmMethod = callback; 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(); getApp().getQuitTimer().reset();
if (mTimer != null) { if (mTimer != null) {
mTimer.stop(); mTimer.stop();
@ -60,6 +68,7 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
return true; return true;
} }
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
function onTimeout() as Void { function onTimeout() as Void {
mTimer.stop(); mTimer.stop();
WatchUi.popView(WatchUi.SLIDE_RIGHT); WatchUi.popView(WatchUi.SLIDE_RIGHT);

View File

@ -11,17 +11,14 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 23 November 2023 // P A Abbey & J D Abbey & Someone0nEarth, 23 November 2023
// //
//
// Description:
//
// Glance view for GarminHomeAssistant
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Lang; using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
//! Glance view for GarminHomeAssistant
//
(:glance) (:glance)
class HomeAssistantGlanceView extends WatchUi.GlanceView { class HomeAssistantGlanceView extends WatchUi.GlanceView {
private static const scLeftMargin = 5; // in pixels 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 mMenuStatus as WatchUi.Text or Null;
private var mAntiAlias as Lang.Boolean = false; private var mAntiAlias as Lang.Boolean = false;
//! Class Constructor
//
function initialize(app as HomeAssistantApp) { function initialize(app as HomeAssistantApp) {
GlanceView.initialize(); GlanceView.initialize();
mApp = app; 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 { function onLayout(dc as Graphics.Dc) as Void {
var h = dc.getHeight(); var h = dc.getHeight();
var tw = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String, Graphics.FONT_XTINY); 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 { function onUpdate(dc as Graphics.Dc) as Void {
GlanceView.onUpdate(dc); GlanceView.onUpdate(dc);
if(mAntiAlias) { if(mAntiAlias) {

View File

@ -11,20 +11,19 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // 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.Lang;
using Toybox.WatchUi; 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 { class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
private var mMenu as HomeAssistantView; private var mMenu as HomeAssistantView;
//! Class Constructor
//
function initialize( function initialize(
definition as Lang.Dictionary, definition as Lang.Dictionary,
template as Lang.String, template as Lang.String,
@ -48,6 +47,8 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
mMenu = new HomeAssistantView(definition, null); mMenu = new HomeAssistantView(definition, null);
} }
//! Return the submenu for this group menu item.
//
function getMenuView() as HomeAssistantView { function getMenuView() as HomeAssistantView {
return mMenu; return mMenu;
} }

View File

@ -11,20 +11,23 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023 // 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.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
//! Generic menu button with an icon that optionally renders a Home Assistant Template.
//
class HomeAssistantMenuItem extends WatchUi.IconMenuItem { class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String or Null; 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( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
template as Lang.String, template as Lang.String,
@ -43,14 +46,27 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
mTemplate = template; 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 { function hasTemplate() as Lang.Boolean {
return mTemplate != null; 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; 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 { function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
if (data == null) { if (data == null) {
setSubLabel($.Rez.Strings.Empty); setSubLabel($.Rez.Strings.Empty);

View File

@ -11,17 +11,14 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 17 November 2023 // P A Abbey & J D Abbey & Someone0nEarth, 17 November 2023
// //
//
// Description:
//
// MenuItems Factory.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Application; using Toybox.Application;
using Toybox.Lang; using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
//! MenuItems Factory class.
//
class HomeAssistantMenuItemFactory { class HomeAssistantMenuItemFactory {
private var mMenuItemOptions as Lang.Dictionary; private var mMenuItemOptions as Lang.Dictionary;
private var mTapTypeIcon as WatchUi.Bitmap; private var mTapTypeIcon as WatchUi.Bitmap;
@ -31,6 +28,8 @@ class HomeAssistantMenuItemFactory {
private static var instance; private static var instance;
//! Class Constructor
//
private function initialize() { private function initialize() {
mMenuItemOptions = { mMenuItemOptions = {
:alignment => Settings.getMenuAlignment() :alignment => Settings.getMenuAlignment()
@ -57,6 +56,8 @@ class HomeAssistantMenuItemFactory {
mHomeAssistantService = new HomeAssistantService(); mHomeAssistantService = new HomeAssistantService();
} }
//! Create the one and only instance of this class.
//
static function create() as HomeAssistantMenuItemFactory { static function create() as HomeAssistantMenuItemFactory {
if (instance == null) { if (instance == null) {
instance = new HomeAssistantMenuItemFactory(); instance = new HomeAssistantMenuItemFactory();
@ -64,6 +65,14 @@ class HomeAssistantMenuItemFactory {
return instance; 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( function toggle(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null, 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( function tap(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
entity as Lang.String or Null, entity_id as Lang.String or Null,
template as Lang.String or Null, template as Lang.String or Null,
service as Lang.String or Null, service as Lang.String or Null,
confirm as Lang.Boolean, confirm as Lang.Boolean,
pin as Lang.Boolean, pin as Lang.Boolean,
data as Lang.Dictionary or Null data as Lang.Dictionary or Null
) as WatchUi.MenuItem { ) as WatchUi.MenuItem {
if (entity != null) { if (entity_id != null) {
if (data == null) { if (data == null) {
data = { "entity_id" => entity }; data = { "entity_id" => entity_id };
} else { } else {
data.put("entity_id", entity); data.put("entity_id", entity_id);
} }
} }
if (service != null) { 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( function group(
definition as Lang.Dictionary, definition as Lang.Dictionary,
template as Lang.String or Null template as Lang.String or Null

View File

@ -11,25 +11,28 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// //
//
// Description:
//
// Pin Confirmation dialog and logic.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
import Toybox.Graphics; using Toybox.Graphics;
import Toybox.Lang; using Toybox.Lang;
import Toybox.WatchUi; using Toybox.WatchUi;
import Toybox.Timer; using Toybox.Timer;
import Toybox.Attention; using Toybox.Attention;
import Toybox.Time; using Toybox.Time;
//! Pin digit used for number 0..9
//
class PinDigit extends WatchUi.Selectable { 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 marginX = stepX * 0.05; // 5% margin on all sides
var marginY = stepY * 0.05; var marginY = stepY * 0.05;
var x = (digit == 0) ? stepX : stepX * ((digit+2) % 3); // layout '0' in 2nd col, others ltr in 3 columns 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; mDigit = digit;
} }
function getDigit() as Number { //! Return the digit 0..9 represented by this button
//
function getDigit() as Lang.Number {
return mDigit; return mDigit;
} }
//! Customised drawing of a PIN digit's button.
//
class PinDigitButton extends WatchUi.Drawable { class PinDigitButton extends WatchUi.Drawable {
private var mText as Number; private var mText as Lang.Number;
private var mTouched as Boolean = false; private var mTouched as Lang.Boolean = false;
//! Class Constructor
//!
//! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.<br>
//! &lbrace;<br>
//! &emsp; :label as Lang.Number, // The digit 0..9 to display<br>
//! &emsp; :touched as Lang.Boolean, // Should the digit be filled to indicate it has been pressed?<br>
//! &emsp; + those required by `Drawable.initialize()`<br>
//! &rbrace;
//
function initialize(options) { function initialize(options) {
Drawable.initialize(options); Drawable.initialize(options);
mText = options.get(:label); mText = options.get(:label);
mTouched = options.get(:touched); mTouched = options.get(:touched);
} }
function draw(dc) { //! Draw the PIN digit button.
//!
//! @param dc Device context
//
function draw(dc as Graphics.Dc) {
if (mTouched) { if (mTouched) {
dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE); dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE);
} else { } else {
@ -98,17 +117,27 @@ class PinDigit extends WatchUi.Selectable {
} }
//! Pin Confirmation dialog and logic.
//
class HomeAssistantPinConfirmationView extends WatchUi.View { 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;
var mPinMask as String = ""; //! Indicates how many digits have been entered so far.
var mPinMask as Lang.String = "";
//! Class Constructor
//
function initialize() { function initialize() {
View.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 stepX = (dc.getWidth() - MARGIN_X * 2) / 3; // three columns
var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry) var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry)
var digits = []; var digits = [];
@ -119,7 +148,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
setLayout(digits); 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); View.onUpdate(dc);
if (mPinMask.length() != 0) { if (mPinMask.length() != 0) {
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); 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 = ""; mPinMask = "";
for (var i=0; i<length; i++) { for (var i=0; i<length; i++) {
mPinMask += "*"; mPinMask += "*";
@ -138,17 +175,31 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
} }
//! Delegate for the HomeAssistantPinConfirmationView.
//
class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate { class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
private var mPin as String; private var mPin as Lang.String;
private var mEnteredPin as String; private var mEnteredPin as Lang.String;
private var mConfirmMethod as Method(state as Lang.Boolean) as Void; private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
private var mTimer as Timer.Timer or Null; private var mTimer as Timer.Timer or Null;
private var mState as Lang.Boolean; private var mState as Lang.Boolean;
private var mFailures as PinFailures; private var mFailures as PinFailures;
private var mView as HomeAssistantPinConfirmationView; private var mView as HomeAssistantPinConfirmationView;
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean, pin as String, view as HomeAssistantPinConfirmationView) { //! Class Constructor
//!
//! @param callback Method to call on confirmation.
//! @param state Current state of a toggle button.
//! @param pin PIN to be matched.
//! @param view PIN confirmation view.
//
function initialize(
callback as Method(state as Lang.Boolean) as Void,
state as Lang.Boolean,
pin as Lang.String,
view as HomeAssistantPinConfirmationView
) {
BehaviorDelegate.initialize(); BehaviorDelegate.initialize();
mFailures = new PinFailures(); mFailures = new PinFailures();
if (mFailures.isLocked()) { if (mFailures.isLocked()) {
@ -165,7 +216,12 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
resetTimer(); resetTimer();
} }
function onSelectable(event as SelectableEvent) as Boolean { //! Add another entered digit to the "PIN so far". When it is long enough verify the PIN is correct and the
//! invoke the supplied call back function.
//!
//! @param event The digit pressed by the user tapping the screen.
//
function onSelectable(event as WatchUi.SelectableEvent) as Lang.Boolean {
if (mFailures.isLocked()) { if (mFailures.isLocked()) {
goBack(); goBack();
} }
@ -173,7 +229,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) { if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) {
mEnteredPin += instance.getDigit(); mEnteredPin += instance.getDigit();
createUserFeedback(); createUserFeedback();
// System.println("HomeAssitantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin); // System.println("HomeAssistantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
if (mEnteredPin.length() == mPin.length()) { if (mEnteredPin.length() == mPin.length()) {
if (mEnteredPin.equals(mPin)) { if (mEnteredPin.equals(mPin)) {
mFailures.reset(); mFailures.reset();
@ -193,6 +249,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
return true; return true;
} }
//! Hepatic feedback.
//
function createUserFeedback() { function createUserFeedback() {
if (Attention has :vibrate && Settings.getVibrate()) { if (Attention has :vibrate && Settings.getVibrate()) {
Attention.vibrate([new Attention.VibeProfile(25, 25)]); Attention.vibrate([new Attention.VibeProfile(25, 25)]);
@ -200,6 +258,9 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
mView.updatePinMask(mEnteredPin.length()); mView.updatePinMask(mEnteredPin.length());
} }
//! A timer is used to clear the PIN entry view if digits are not pressed. So each time a digit is pressed the
//! timer is reset.
//
function resetTimer() { function resetTimer() {
var timeout = Settings.getConfirmTimeout(); // ms var timeout = Settings.getConfirmTimeout(); // ms
if (timeout > 0) { if (timeout > 0) {
@ -212,6 +273,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
} }
} }
//! Cancel PIN entry.
//
function goBack() as Void { function goBack() as Void {
if (mTimer != null) { if (mTimer != null) {
mTimer.stop(); mTimer.stop();
@ -219,6 +282,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
WatchUi.popView(WatchUi.SLIDE_RIGHT); WatchUi.popView(WatchUi.SLIDE_RIGHT);
} }
//! Hepatic feedback for a wrong PIN and cancel entry.
//
function error() as Void { function error() as Void {
// System.println("HomeAssistantPinConfirmationDelegate error() Wrong PIN entered"); // System.println("HomeAssistantPinConfirmationDelegate error() Wrong PIN entered");
mFailures.addFailure(); mFailures.addFailure();
@ -241,14 +306,19 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
} }
//! Manage PIN entry failures to try and prevent brute force exhaustion by inserting delays in retries.
//
class PinFailures { class PinFailures {
const STORAGE_KEY_FAILURES as String = "pin_failures"; const STORAGE_KEY_FAILURES as Lang.String = "pin_failures";
const STORAGE_KEY_LOCKED as String = "pin_locked"; const STORAGE_KEY_LOCKED as Lang.String = "pin_locked";
private var mFailures as Array<Number>; private var mFailures as Lang.Array<Lang.Number>;
private var mLockedUntil as Number or Null; private var mLockedUntil as Lang.Number or Null;
//! Class Constructor
//
function initialize() { function initialize() {
// System.println("PinFailures initialize() Initializing PIN failures from storage"); // System.println("PinFailures initialize() Initializing PIN failures from storage");
var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES); var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES);
@ -256,6 +326,8 @@ class PinFailures {
mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED); mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED);
} }
//! Record a PIN entry failure. If too many have occurred lock the application.
//
function addFailure() { function addFailure() {
mFailures.add(Time.now().value()); mFailures.add(Time.now().value());
// System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded"); // System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded");
@ -268,7 +340,7 @@ class PinFailures {
mFailures = mFailures.slice(1, null); mFailures = mFailures.slice(1, null);
} else { } else {
mFailures = []; 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); Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil);
// System.println("PinFailures addFailure() Locked until " + mLockedUntil); // System.println("PinFailures addFailure() Locked until " + mLockedUntil);
} }
@ -276,6 +348,9 @@ class PinFailures {
Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures); 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() { function reset() {
// System.println("PinFailures reset() Resetting failures"); // System.println("PinFailures reset() Resetting failures");
mFailures = []; mFailures = [];
@ -284,11 +359,18 @@ class PinFailures {
Application.Storage.deleteValue(STORAGE_KEY_LOCKED); 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(); 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) { if (mLockedUntil == null) {
return false; return false;
} }

View File

@ -11,11 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023 // P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
// //
//
// Description:
//
// Calling a Home Assistant Service.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Lang; using Toybox.Lang;
@ -23,10 +18,14 @@ using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
using Toybox.Application.Properties; using Toybox.Application.Properties;
//! Calling a Home Assistant Service.
//
class HomeAssistantService { class HomeAssistantService {
private var mHasToast as Lang.Boolean = false; private var mHasToast as Lang.Boolean = false;
private var mHasVibrate as Lang.Boolean = false; private var mHasVibrate as Lang.Boolean = false;
//! Class Constructor
//
function initialize() { function initialize() {
if (WatchUi has :showToast) { if (WatchUi has :showToast) {
mHasToast = true; 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( function onReturnCall(
responseCode as Lang.Number, 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( function call(
service as Lang.String, service as Lang.String,
data as Lang.Dictionary or Null data as Lang.Dictionary or Null

View File

@ -11,17 +11,14 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// //
//
// Description:
//
// Menu button that triggers a service.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Lang; using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
//! Menu button that triggers a service.
//
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService; private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String or Null; private var mService as Lang.String or Null;
@ -29,6 +26,19 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mPin as Lang.Boolean; private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary or Null; 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( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
template as Lang.String, template as Lang.String,
@ -62,6 +72,8 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
mData = data; mData = data;
} }
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
//
function callService() as Void { function callService() as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen; var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) { 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 { function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) { if (mService != null) {
mHomeAssistantService.call(mService, mData); mHomeAssistantService.call(mService, mData);

View File

@ -11,11 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // 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; using Toybox.Lang;
@ -24,6 +19,8 @@ using Toybox.Graphics;
using Toybox.Application.Properties; using Toybox.Application.Properties;
using Toybox.Timer; 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 { class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mConfirm as Lang.Boolean; private var mConfirm as Lang.Boolean;
private var mPin 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 mTemplate as Lang.String;
private var mHasVibrate as Lang.Boolean = false; 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( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
template as Lang.String, template as Lang.String,
@ -58,6 +64,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
mTemplate = template; mTemplate = template;
} }
//! Set the state of a toggle menu item.
//
private function setUiToggle(state as Null or Lang.String) as Void { private function setUiToggle(state as Null or Lang.String) as Void {
if (state != null) { if (state != null) {
if (state.equals("on") && !isEnabled()) { 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; 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") + "')}}"; 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 { function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
if (data == null) { if (data == null) {
setSubLabel(null); setSubLabel(null);
@ -100,6 +121,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
WatchUi.requestUpdate(); 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 { function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) { if (data == null) {
setUiToggle("off"); setUiToggle("off");
@ -126,9 +151,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
WatchUi.requestUpdate(); 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 Code: " + responseCode);
// System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Data: " + data); // System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Data: " + data);
@ -183,6 +214,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
getApp().setApiStatus(status); 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 { function setState(s as Lang.Boolean) as Void {
// Toggle the UI back, we'll wait for confirmation from the Home Assistant // Toggle the UI back, we'll wait for confirmation from the Home Assistant
setEnabled(!isEnabled()); 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 { function callService(b as Lang.Boolean) as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen; var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) { 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 { function onConfirm(b as Lang.Boolean) as Void {
setState(b); setState(b);
} }

View File

@ -11,11 +11,6 @@
// //
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// //
//
// Description:
//
// Home Assistant menu construction.
//
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
using Toybox.Application; using Toybox.Application;
@ -24,8 +19,12 @@ using Toybox.Graphics;
using Toybox.System; using Toybox.System;
using Toybox.WatchUi; using Toybox.WatchUi;
//! Home Assistant menu construction.
//
class HomeAssistantView extends WatchUi.Menu2 { class HomeAssistantView extends WatchUi.Menu2 {
//! Class Constructor
//
function initialize( function initialize(
definition as Lang.Dictionary, definition as Lang.Dictionary,
options as { 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<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> { function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> {
var fullList = []; var fullList = [];
var lmi = mItems as Lang.Array<WatchUi.MenuItem>; var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
@ -102,26 +106,35 @@ class HomeAssistantView extends WatchUi.Menu2 {
return fullList; return fullList;
} }
// Called when this View is brought to the foreground. Restore //! Called when this View is brought to the foreground. Restore
// the state of this View and prepare it to be shown. This includes //! the state of this View and prepare it to be shown. This includes
// loading resources into memory. //! loading resources into memory.
function onShow() as Void {} function onShow() as Void {}
} }
// //! Delegate for the HomeAssistantView.
// Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/ //!
//! Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
// //
class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
private var mIsRootMenuView as Lang.Boolean = false; private var mIsRootMenuView as Lang.Boolean = false;
private var mTimer as QuitTimer; 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) { function initialize(isRootMenuView as Lang.Boolean) {
Menu2InputDelegate.initialize(); Menu2InputDelegate.initialize();
mIsRootMenuView = isRootMenuView; mIsRootMenuView = isRootMenuView;
mTimer = getApp().getQuitTimer(); mTimer = getApp().getQuitTimer();
} }
//! Back button event
//
function onBack() { function onBack() {
mTimer.reset(); mTimer.reset();
@ -135,16 +148,22 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
WatchUi.popView(WatchUi.SLIDE_RIGHT); WatchUi.popView(WatchUi.SLIDE_RIGHT);
} }
// Only for CheckboxMenu //! Only for CheckboxMenu
//
function onDone() { function onDone() {
mTimer.reset(); mTimer.reset();
} }
// Only for CustomMenu //! Only for CustomMenu
//
function onFooter() { function onFooter() {
mTimer.reset(); mTimer.reset();
} }
//! Select event
//!
//! @param item Selected menu item.
//
function onSelect(item as WatchUi.MenuItem) as Void { function onSelect(item as WatchUi.MenuItem) as Void {
mTimer.reset(); mTimer.reset();
if (item instanceof HomeAssistantToggleMenuItem) { if (item instanceof HomeAssistantToggleMenuItem) {
@ -164,7 +183,8 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
} }
} }
// Only for CustomMenu //! Only for CustomMenu
//
function onTitle() { function onTitle() {
mTimer.reset(); mTimer.reset();
} }

View File

@ -11,11 +11,6 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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; using Toybox.Lang;
@ -23,18 +18,27 @@ using Toybox.Timer;
using Toybox.Application.Properties; using Toybox.Application.Properties;
using Toybox.WatchUi; using Toybox.WatchUi;
//! Quit the application after a period of inactivity in order to save the battery.
//!
class QuitTimer extends Timer.Timer { class QuitTimer extends Timer.Timer {
//! Class Constructor
//
function initialize() { function initialize() {
Timer.Timer.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 { function exitApp() as Void {
// System.println("QuitTimer exitApp(): Exiting"); // System.println("QuitTimer exitApp(): Exiting");
// This will exit the system cleanly from any point within an app. // This will exit the system cleanly from any point within an app.
System.exit(); System.exit();
} }
//! Kick off the quit timer.
//
function begin() { function begin() {
var api_timeout = Settings.getAppTimeout(); // ms var api_timeout = Settings.getAppTimeout(); // ms
if (api_timeout > 0) { if (api_timeout > 0) {
@ -42,6 +46,8 @@ class QuitTimer extends Timer.Timer {
} }
} }
//! Reset the quit timer.
//
function reset() { function reset() {
// System.println("QuitTimer reset(): Restarted quit timer"); // System.println("QuitTimer reset(): Restarted quit timer");
stop(); stop();

View File

@ -11,35 +11,36 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Math; 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 { class ScalableView extends WatchUi.View {
//! Retain the local screen width for efficiency
private var mScreenWidth; private var mScreenWidth;
//! Class Constructor
//
function initialize() { function initialize() {
View.initialize(); View.initialize();
mScreenWidth = System.getDeviceSettings().screenWidth; mScreenWidth = System.getDeviceSettings().screenWidth;
} }
// Convert a fraction expressed as a percentage (%) to a number of pixels for the //! Convert a fraction expressed as a percentage (%) to a number of pixels for the
// screen's dimensions. //! screen's dimensions.
// //!
// Parameters: //! Uses screen width rather than screen height as rectangular screens tend to have
// * dc - Device context //! height > width.
// * pc - Percentage (%) expressed as a number in the range 0.0..100.0 //!
// //! @param 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. //! @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 { function pixelsForScreen(pc as Lang.Float) as Lang.Number {
return Math.round(pc * mScreenWidth) / 100; return Math.round(pc * mScreenWidth) / 100;
} }

View File

@ -11,16 +11,6 @@
// //
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 23 November 2023 // 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; using Toybox.Lang;
@ -31,6 +21,11 @@ using Toybox.System;
using Toybox.Background; using Toybox.Background;
using Toybox.Time; using Toybox.Time;
//! Home Assistant settings.
//!
//! <em>WARNING!</em> 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) (:glance, :background)
class Settings { class Settings {
private static var mApiKey as Lang.String = ""; private static var mApiKey as Lang.String = "";
@ -40,19 +35,24 @@ class Settings {
private static var mCacheConfig as Lang.Boolean = false; private static var mCacheConfig as Lang.Boolean = false;
private static var mClearCache as Lang.Boolean = false; private static var mClearCache as Lang.Boolean = false;
private static var mVibrate as Lang.Boolean = false; private static var mVibrate as Lang.Boolean = false;
private static var mAppTimeout as Lang.Number = 0; // seconds //! seconds
private static var mPollDelay as Lang.Number = 0; // seconds private static var mAppTimeout as Lang.Number = 0;
private static var mConfirmTimeout as Lang.Number = 3; // seconds //! 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 mPin as Lang.String or Null = "0000";
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT; 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 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 mIsApp as Lang.Boolean = false;
private static var mHasService 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; 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() { static function update() {
mIsApp = getApp().getIsApp(); mIsApp = getApp().getIsApp();
mApiKey = Properties.getValue("api_key"); mApiKey = Properties.getValue("api_key");
@ -71,6 +71,8 @@ class Settings {
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate"); mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
} }
//! A webhook is required for non-privileged API calls.
//
static function webhook() { static function webhook() {
if (System has :ServiceDelegate) { if (System has :ServiceDelegate) {
mHasService = true; 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 { static function getApiKey() as Lang.String {
return mApiKey; return mApiKey;
} }
//! Get the Webhook ID supplied as part of the Settings.
//!
//! @return The Webhook ID
//
static function getWebhookId() as Lang.String { static function getWebhookId() as Lang.String {
return mWebhookId; 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) { static function setWebhookId(webhookId as Lang.String) {
mWebhookId = webhookId; mWebhookId = webhookId;
Properties.setValue("webhook_id", mWebhookId); Properties.setValue("webhook_id", mWebhookId);
} }
//! Delete the Webhook ID saved as part of the Settings.
//
static function unsetWebhookId() { static function unsetWebhookId() {
mWebhookId = ""; mWebhookId = "";
Properties.setValue("webhook_id", 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 { static function getApiUrl() as Lang.String {
return mApiUrl; return mApiUrl;
} }
//! Get the menu configuration URL supplied as part of the Settings.
//!
//! @return The menu configuration URL
//
static function getConfigUrl() as Lang.String { static function getConfigUrl() as Lang.String {
return mConfigUrl; 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 { static function getCacheConfig() as Lang.Boolean {
return mCacheConfig; 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 { static function getClearCache() as Lang.Boolean {
return mClearCache; return mClearCache;
} }
//! Unset the clear cache Boolean option supplied as part of the Settings.
//
static function unsetClearCache() { static function unsetClearCache() {
mClearCache = false; mClearCache = false;
Properties.setValue("clear_cache", mClearCache); 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 { static function getVibrate() as Lang.Boolean {
return mVibrate; 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 { static function getAppTimeout() as Lang.Number {
return mAppTimeout * 1000; // Convert to milliseconds 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 { static function getPollDelay() as Lang.Number {
return mPollDelay * 1000; // Convert to milliseconds 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 { static function getConfirmTimeout() as Lang.Number {
return mConfirmTimeout * 1000; // Convert to milliseconds 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 { static function getPin() as Lang.String or Null {
return mPin; 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 { private static function validatePin() as Lang.String or Null {
var pin = Properties.getValue("pin"); var pin = Properties.getValue("pin");
if (pin.toNumber() == null || pin.length() != 4) { if (pin.toNumber() == null || pin.length() != 4) {
@ -183,14 +243,24 @@ class Settings {
return pin; return pin;
} }
//! Get the menu item alignment as part of the Settings.
//!
//! @return The menu item alignment.
//
static function getMenuAlignment() as Lang.Number { static function getMenuAlignment() as Lang.Number {
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT 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 { static function isSensorsLevelEnabled() as Lang.Boolean {
return mIsSensorsLevelEnabled; return mIsSensorsLevelEnabled;
} }
//! Disable logging of the watch's sensors.
//
static function unsetIsSensorsLevelEnabled() { static function unsetIsSensorsLevelEnabled() {
mIsSensorsLevelEnabled = false; mIsSensorsLevelEnabled = false;
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled); Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);

View File

@ -11,88 +11,99 @@
// //
// J D Abbey & P A Abbey, 28 December 2022 // 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
//
// <?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
//
// https://domain.name/path/log_clear.php
//
// <?php
// $myfile = fopen("log", "w");
// fwrite($myfile, "");
// print "Success";
// ?>
using Toybox.Communications; using Toybox.Communications;
using Toybox.Lang; using Toybox.Lang;
using Toybox.System; 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:
//! <pre>
//! wl = new WebLog();
//! wl.clear();
//! wl.println("Debug Message");
//! wl.flush();
//! </pre>
//!
//! File: https://domain.name/path/log.php
//!
//! <pre>
//! &lt;?php
//! $myfile = fopen("log", "a");
//! $queries = array();
//! parse_str($_SERVER['QUERY_STRING'], $queries);
//! fwrite($myfile, $queries['log']);
//! print "Success";
//! ?&gt;
//! </pre>
//!
//! Logs published to https://domain.name/path/log.
//!
//! File: https://domain.name/path/log_clear.php
//!
//! <pre>
//! &lt;?php
//! $myfile = fopen("log", "w");
//! fwrite($myfile, "");
//! print "Success";
//! ?&gt;
//! </pre>
//
(:glance, :background) (:glance, :background)
class WebLog { 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 numCalls = 0 as Lang.Number;
private var buffer = "" as Lang.String; private var buffer = "" as Lang.String;
// Set the number of calls to print() before sending the buffer to the online //! Set the number of calls to print() before sending the buffer to the online
// logger. //! logger.
//!
//! @param l The number of log calls to buffer before writing to the online service.
// //
function setCallsBuffer(l as Lang.Number) { function setCallsBuffer(l as Lang.Number) {
callsbuffer = l; callsBuffer = l;
} }
// Get the number of calls to print() before sending the buffer to the online //! Get the number of calls to print() before sending the buffer to the online
// logger. //! logger.
//!
//! @return The number of log calls to buffer before writing to the online service.
// //
function getCallsBuffer() as Lang.Number { function getCallsBuffer() as Lang.Number {
return callsbuffer; return callsBuffer;
} }
// Create a debug log over the Internet to keep track of the watch's runtime //! Create a debug log over the Internet to keep track of the watch's runtime
// execution. //! execution.
//!
//! @param str The string to log.
// //
function print(str as Lang.String) { function print(str as Lang.String) {
var myTime = System.getClockTime(); var myTime = System.getClockTime();
buffer += myTime.hour.format("%02d") + ":" + myTime.min.format("%02d") + ":" + myTime.sec.format("%02d") + " " + str; buffer += myTime.hour.format("%02d") + ":" + myTime.min.format("%02d") + ":" + myTime.sec.format("%02d") + " " + str;
numCalls++; numCalls++;
// System.println("WebLog print() str = " + str); // System.println("WebLog print() str = " + str);
if (numCalls >= callsbuffer) { if (numCalls >= callsBuffer) {
doPrint(); doPrint();
} }
} }
// Create a debug log over the Internet to keep track of the watch's runtime //! Create a debug log over the Internet to keep track of the watch's runtime
// execution. Add a new line character to the end. //! execution. Add a new line character to the end.
//!
//! @param str The string to log.
// //
function println(str as Lang.String) { function println(str as Lang.String) {
print(str + "\n"); print(str + "\n");
} }
// Flush the current buffer to the online logger even if it has not reach the //! Flush the current buffer to the online logger even if it has not reach the
// submission level set by 'callsbuffer'. //! submission level set by 'callsBuffer'.
// //
function flush() { function flush() {
// System.println("WebLog 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() { function doPrint() {
// System.println("WebLog doPrint()"); // System.println("WebLog doPrint()");
@ -122,8 +133,8 @@ class WebLog {
buffer = ""; buffer = "";
} }
// Clear the debug log over the Internet to start a new track of the watch's runtime //! Clear the debug log over the Internet to start a new track of the watch's runtime
// execution. //! execution.
// //
function clear() { function clear() {
// System.println("WebLog clear()"); // System.println("WebLog clear()");
@ -143,7 +154,10 @@ class WebLog {
buffer = ""; 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 { function onLog(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// if (responseCode != 200) { // 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 { function onClear(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// if (responseCode != 200) { // if (responseCode != 200) {

View File

@ -11,14 +11,6 @@
// //
// P A Abbey & J D Abbey, 10 January 2024 // 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; using Toybox.Lang;
@ -26,10 +18,24 @@ using Toybox.Communications;
using Toybox.System; using Toybox.System;
using Toybox.WatchUi; 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 { 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) { switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE: 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() { function requestWebhookId() {
var deviceSettings = System.getDeviceSettings(); var deviceSettings = System.getDeviceSettings();
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier); // 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<Lang.Object>) 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<Lang.Object>
) as Void {
switch (responseCode) { switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT: case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE: case Communications.BLE_CONNECTION_UNAVAILABLE:
@ -194,7 +213,11 @@ class WebhookManager {
} }
} }
function registerWebhookSensor(sensors as Lang.Array<Lang.Object>) { //! 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<Lang.Object>) {
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(); var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString()); // System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
// System.println("WebhookManager registerWebhookSensor(): URL=" + url); // 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() { function registerWebhookSensors() {
var heartRate = Activity.getActivityInfo().currentHeartRate; var heartRate = Activity.getActivityInfo().currentHeartRate;