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": "."
}
],
"settings": {}
"settings": {
"cSpell.words": [
"Initialiser"
]
}
}

View File

@@ -11,15 +11,6 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// Alert provides a means to present application notifications to the user
// briefly. Credit to travis.vitek on forums.garmin.com.
//
// Reference:
// * https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -27,6 +18,12 @@ using Toybox.Graphics;
using Toybox.WatchUi;
using Toybox.Timer;
//! The Alert class provides a means to present application notifications to the user
//! briefly. Credit to travis.vitek on forums.garmin.com.
//!
//! Reference:
//! @url https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages
//
class Alert extends WatchUi.View {
private static const scRadius = 10;
private var mTimer as Timer.Timer;
@@ -36,6 +33,16 @@ class Alert extends WatchUi.View {
private var mFgcolor as Graphics.ColorType;
private var mBgcolor as Graphics.ColorType;
//! Class Constructor
//! @param params A dictionary object as follows:<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) {
View.initialize();
@@ -67,14 +74,22 @@ class Alert extends WatchUi.View {
mTimer = new Timer.Timer();
}
//! Setup a timer to dismiss the alert.
//
function onShow() {
mTimer.start(method(:dismiss), mTimeout, false);
}
//! Prematurely stop the timer.
//
function onHide() {
mTimer.stop();
}
//! Draw the Alert view.
//!
//! @param dc Device context
//
function onUpdate(dc as Graphics.Dc) {
var tWidth = dc.getTextWidthInPixels(mText, mFont);
var tHeight = dc.getFontHeight(mFont);
@@ -110,32 +125,49 @@ class Alert extends WatchUi.View {
dc.drawText(tX, tY, mFont, mText, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
}
// Remove the alert from view, usually on user input, but that is defined by the calling function.
//! Remove the alert from view, usually on user input, but that is defined by the calling function.
//
function dismiss() as Void {
WatchUi.popView(SLIDE_IMMEDIATE);
}
function pushView(transition) as Void {
//! Push this view onto the view stack.
//!
//! @param transition Slide Type
function pushView(transition as WatchUi.SlideType) as Void {
WatchUi.pushView(self, new AlertDelegate(self), transition);
}
}
//! Input Delegate for the Alert view.
//
class AlertDelegate extends WatchUi.InputDelegate {
private var mView;
private var mView as Alert;
function initialize(view) {
//! Class Constructor
//!
//! @param view The Alert view for which this class is a delegate.
//!
function initialize(view as Alert) {
InputDelegate.initialize();
mView = view;
}
function onKey(evt) as Lang.Boolean {
//! Handle key events.
//!
//! @param evt The key event whose value is ignored, just fact of key event matters.
//!
function onKey(evt as WatchUi.KeyEvent) as Lang.Boolean {
mView.dismiss();
getApp().getQuitTimer().reset();
return true;
}
function onTap(evt) as Lang.Boolean {
//! Handle click events.
//!
//! @param evt The click event whose value is ignored, just fact of key event matters.
//!
function onTap(evt as WatchUi.ClickEvent) as Lang.Boolean {
mView.dismiss();
getApp().getQuitTimer().reset();
return true;

View File

@@ -11,12 +11,6 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
//
//
// Description:
//
// The background service delegate currently just reports the Garmin watch's battery
// level.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -25,20 +19,43 @@ using Toybox.Background;
using Toybox.System;
using Toybox.Activity;
//! The background service delegate reports the Garmin watch's various status values
//! back to the Home Assistant instance.
//
(:background)
class BackgroundServiceDelegate extends System.ServiceDelegate {
//! Class Constructor
//
function initialize() {
ServiceDelegate.initialize();
}
function onReturnBatteryUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Code: " + responseCode);
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Data: " + data);
//! Callback function for doUpdate().
//!
//! @param responseCode Response code
//! @param data Return data
//
function onReturnDoUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Code: " + responseCode);
// System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Data: " + data);
Background.exit(null);
}
function onActivityCompleted(activity as { :sport as Activity.Sport, :subSport as Activity.SubSport }) as Void {
//! Called on completion of an activity.
//!
//! @param activity Specified as a Dictionary with two items.<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) {
// System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call.");
} else if (!System.getDeviceSettings().connectionAvailable) {
@@ -50,6 +67,8 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
}
}
//! Called periodically to send status updates to the Home Assistant instance.
//
function onTemporalEvent() as Void {
if (!System.getDeviceSettings().phoneConnected) {
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call.");
@@ -76,7 +95,15 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
}
}
private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) {
//! Combined update function to collect the data to be sent as updates to the Home Assistant instance.
//!
//! @param activity Activity.Sport
//! @param sub_activity Activity.SubSport
//
private function doUpdate(
activity as Lang.Number or Null,
sub_activity as Lang.Number or Null
) {
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
var position = Position.getInfo();
// System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees());
@@ -136,7 +163,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnBatteryUpdate)
method(:onReturnDoUpdate)
);
}
var activityInfo = ActivityMonitor.getInfo();
@@ -222,7 +249,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnBatteryUpdate)
method(:onReturnDoUpdate)
);
}

View File

@@ -11,15 +11,12 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// ClientId is somewhere to store personal credentials that should not be shared in
// a separate file that is locally customised to the source code and not commited
// back to GitHub.
//
//-----------------------------------------------------------------------------------
//! ClientId is somewhere to store personal credentials that should not be shared in
//! a separate file that is locally customised to the source code and not committed
//! back to GitHub.
//
(:glance)
class ClientId {
static const webLogUrl = "https://...";

View File

@@ -11,22 +11,6 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// ErrorView provides a means to present application errors to the user. These
// should not happen of course... but they do, so best make sure errors can be
// reported.
//
// Designed so that a single ErrorView is used for all errors and hence can ensure
// that only the first call to display is honoured until the view is dismissed.
// This compensates for older devices not being able to call WatchUi.getCurrentView()
// due to not supporting API level 3.4.0.
//
// Usage:
// 1) ErrorView.show("Error message");
// 2) return ErrorView.create("Error message"); // as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>
//
//-----------------------------------------------------------------------------------
using Toybox.Graphics;
@@ -35,6 +19,19 @@ using Toybox.WatchUi;
using Toybox.Communications;
using Toybox.Timer;
//! ErrorView provides a means to present application errors to the user. These
//! should not happen of course... but they do, so best make sure errors can be
//! reported.
//!
//! Designed so that a single ErrorView is used for all errors and hence can ensure
//! that only the first call to display is honoured until the view is dismissed.
//! This compensates for older devices not being able to call WatchUi.getCurrentView()
//! due to not supporting API level 3.4.0.
//!
//! Usage:
//! 1) `ErrorView.show("Error message");`
//! 2) `return ErrorView.create("Error message"); // as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>`
//
class ErrorView extends ScalableView {
private static const scErrorIconMargin as Lang.Float = 7f;
private var mText as Lang.String = "";
@@ -48,9 +45,11 @@ class ErrorView extends ScalableView {
private static var instance;
private static var mShown as Lang.Boolean = false;
//! Class Constructor
//
function initialize() {
ScalableView.initialize();
mDelegate = new ErrorDelegate(self);
mDelegate = new ErrorDelegate();
// Convert the settings from % of screen size to pixels
mErrorIconMargin = pixelsForScreen(scErrorIconMargin);
mErrorIcon = Application.loadResource(Rez.Drawables.ErrorIcon) as Graphics.BitmapResource;
@@ -59,7 +58,10 @@ class ErrorView extends ScalableView {
}
}
// Load your resources here
//! Construct the view.
//!
//! @param dc Device context
//
function onLayout(dc as Graphics.Dc) as Void {
var w = dc.getWidth();
@@ -75,7 +77,10 @@ class ErrorView extends ScalableView {
});
}
// Update the view
//! Update the view
//!
//! @param dc Device context
//
function onUpdate(dc as Graphics.Dc) as Void {
var w = dc.getWidth();
if (mAntiAlias) {
@@ -87,10 +92,18 @@ class ErrorView extends ScalableView {
mTextArea.draw(dc);
}
//! Get this view's delegate for processing events.
//
function getDelegate() as ErrorDelegate {
return mDelegate;
}
//! 'Create' (get) the ErrorView instance, intended to make short work of using this class. E.g.
//!
//! `return ErrorView.create("Went wrong!");`
//!
//! @param text The string to display in the ErrorView.
//
static function create(text as Lang.String) as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
if (instance == null) {
instance = new ErrorView();
@@ -102,7 +115,10 @@ class ErrorView extends ScalableView {
return [instance, instance.getDelegate()];
}
// Create or reuse an existing ErrorView, and pass on the text.
//! Create or reuse an existing ErrorView, and pass on the text.
//!
//! @param text The string to display in the ErrorView.
//
static function show(text as Lang.String) as Void {
if (!mShown) {
create(text); // Ignore returned values
@@ -113,6 +129,8 @@ class ErrorView extends ScalableView {
}
}
//! Pop the view and clean up timers.
//
static function unShow() as Void {
if (mShown) {
WatchUi.popView(WatchUi.SLIDE_DOWN);
@@ -126,7 +144,10 @@ class ErrorView extends ScalableView {
}
}
// Internal show now we're not a static method like 'show()'.
//! Internal show now we're not a static method like 'show()'.
//!
//! @param text Change the string tio display in the ErrorView.
//
function setText(text as Lang.String) as Void {
mText = text;
if (mTextArea != null) {
@@ -137,12 +158,19 @@ class ErrorView extends ScalableView {
}
//! Delegate for the ErrorView.
//
class ErrorDelegate extends WatchUi.BehaviorDelegate {
function initialize(view as ErrorView) {
//! Class Constructor
//!
function initialize() {
WatchUi.BehaviorDelegate.initialize();
}
//! Process the event to clear the ErrorView.
//
function onBack() as Lang.Boolean {
getApp().getQuitTimer().reset();
ErrorView.unShow();

View File

@@ -11,30 +11,38 @@
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
//
//
// Description:
//
// Home Assistant centralised constants.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
//! Home Assistant centralised constants.
//
(:glance)
class Globals {
//! Alert is a toast at the top of the watch screen, it stays present until tapped
//! or this timeout has expired.
static const scAlertTimeout = 2000; // ms
static const scTapTimeout = 1000; // ms
// Time to let the existing HTTP responses get serviced after a
// Communications.NETWORK_RESPONSE_OUT_OF_MEMORY response code.
//! Time to let the existing HTTP responses get serviced after a
//! `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` response code.
static const scApiBackoff = 1000; // ms
// Needs to be long enough to enable a "double ESC" to quit the application from
// an ErrorView.
//! Needs to be long enough to enable a "double ESC" to quit the application from
//! an ErrorView.
static const scApiResume = 200; // ms
// Warn the user after fetching the menu if their watch is low on memory before the device crashes.
//! Warn the user after fetching the menu if their watch is low on memory before the device crashes.
static const scLowMem = 0.90; // percent as a fraction.
// Constants for PIN confirmation dialog
static const scPinMaxFailures = 5; // Maximum number of failed PIN confirmation attemps allwed in ...
static const scPinMaxFailureMinutes = 2; // ... this number of minutes before PIN confirmation is locked for ...
static const scPinLockTimeMinutes = 10; // ... this number of minutes
//! Constant for PIN confirmation dialog.<br>
//! Maximum number of failed PIN confirmation attempts allowed in `scPinMaxFailureMinutes`.
static const scPinMaxFailures = 5;
//! 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
//
//
// Description:
//
// Application root for GarminHomeAssistant
//
//-----------------------------------------------------------------------------------
using Toybox.Application;
@@ -25,6 +20,8 @@ using Toybox.System;
using Toybox.Application.Properties;
using Toybox.Timer;
//! Application root for GarminHomeAssistant
//
(:glance, :background)
class HomeAssistantApp extends Application.AppBase {
private var mApiStatus as Lang.String or Null;
@@ -40,6 +37,8 @@ class HomeAssistantApp extends Application.AppBase {
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
private var mTemplates as Lang.Dictionary = {};
//! Class Constructor
//
function initialize() {
AppBase.initialize();
// ATTENTION when adding stuff into this block:
@@ -55,7 +54,10 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)".
}
// onStart() is called on application start up
//! Called on application start up
//!
//! @param state see `AppBase.onStart()`
//
function onStart(state as Lang.Dictionary?) as Void {
AppBase.onStart(state);
// ATTENTION when adding stuff into this block:
@@ -71,7 +73,11 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)".
}
// onStop() is called when your application is exiting
//! Called when your application is exiting
//
//!
//! @param state see `AppBase.onStop()`
//
function onStop(state as Lang.Dictionary?) as Void {
AppBase.onStop(state);
// ATTENTION when adding stuff into this block:
@@ -87,7 +93,10 @@ class HomeAssistantApp extends Application.AppBase {
// with "(:glance)".
}
// Return the initial view of your application here
//! Returns the initial view of the application.
//!
//! @return The initial view.
//
function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
mIsApp = true;
mQuitTimer = new QuitTimer();
@@ -132,10 +141,16 @@ class HomeAssistantApp extends Application.AppBase {
}
}
// Callback function after completing the GET request to fetch the configuration menu.
//! Callback function after completing the GET request to fetch the configuration menu.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
(:glance)
function onReturnFetchMenuConfig(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
function onReturnFetchMenuConfig(
responseCode as Lang.Number,
data as Null or Lang.Dictionary or Lang.String
) as Void {
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data);
@@ -208,8 +223,11 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.requestUpdate();
}
// Return true if the menu came from the cache, otherwise false. This is because fetching the menu when not in the cache is
// asynchronous and affects how the views are managed.
//! Fetch the menu configuration over HTTPS, which might be locally cached.
//!
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
//! the menu when not in the cache is asynchronous and affects how the views are managed.
//
(:glance)
function fetchMenuConfig() as Lang.Boolean {
// System.println("Menu URL = " + Settings.getConfigUrl());
@@ -263,6 +281,10 @@ class HomeAssistantApp extends Application.AppBase {
return false;
}
//! Build the menu and store in `mHaMenu`. Then start updates if necessary.
//!
//! @param menu The dictionary derived from the JSON menu fetched by `fetchMenuConfig()`.
//
private function buildMenu(menu as Lang.Dictionary) {
mHaMenu = new HomeAssistantView(menu, null);
mQuitTimer.begin();
@@ -271,6 +293,8 @@ class HomeAssistantApp extends Application.AppBase {
} // If not, this will be done via a chain in Settings.webhook() and mWebhookManager.requestWebhookId() that registers the sensors.
}
//! Start the periodic menu updates for as long as the application is running.
//
function startUpdates() {
if (mHaMenu != null and !mUpdating) {
// Start the continuous update process that continues for as long as the application is running.
@@ -279,7 +303,15 @@ class HomeAssistantApp extends Application.AppBase {
}
}
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
//! Callback function for each menu update GET request.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
function onReturnUpdateMenuItems(
responseCode as Lang.Number,
data as Null or Lang.Dictionary
) as Void {
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
@@ -353,6 +385,8 @@ class HomeAssistantApp extends Application.AppBase {
setApiStatus(status);
}
//! Construct the GET request to update all menu items.
//
function updateMenuItems() as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
@@ -368,7 +402,7 @@ class HomeAssistantApp extends Application.AppBase {
mTemplates = {};
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var template = item.buildTemplate();
var template = item.getTemplate();
if (template != null) {
mTemplates.put(i.toString(), {
"template" => template
@@ -376,7 +410,7 @@ class HomeAssistantApp extends Application.AppBase {
}
if (item instanceof HomeAssistantToggleMenuItem) {
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)
function onReturnFetchApiStatus(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
function onReturnFetchApiStatus(
responseCode as Lang.Number,
data as Null or Lang.Dictionary or Lang.String
) as Void {
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data);
@@ -466,6 +506,8 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.requestUpdate();
}
//! Construct the GET request to test the API status, is it accessible?
//
(:glance)
function fetchApiStatus() as Void {
// System.println("API URL = " + Settings.getApiUrl());
@@ -506,30 +548,49 @@ class HomeAssistantApp extends Application.AppBase {
}
}
//! Record the API status result.
//!
//! @param s A string describing the API status
//
function setApiStatus(s as Lang.String) {
mApiStatus = s;
}
//! Return the API status result.
//!
//! @return A string describing the API status
//
(:glance)
function getApiStatus() as Lang.String {
return mApiStatus;
}
//! Return the Menu status result.
//!
//! @return A string describing the Menu status
//
(:glance)
function getMenuStatus() as Lang.String {
return mMenuStatus;
}
//! Return the Menu construction status.
//!
//! @return A Boolean indicating if the menu is loaded into the application.
//
function isHomeAssistantMenuLoaded() as Lang.Boolean {
return mHaMenu != null;
}
//! Make the menu visible on the watch face.
//
function pushHomeAssistantMenuView() as Void {
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
}
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take
// alternative action if the test fails.
//! Force status updates. Only take action if `Settings.getPollDelay() > 0`. This must be tested
//! locally as it is then efficient to take alternative action if the test fails.
//
function forceStatusUpdates() as Void {
// Don't mess with updates unless we are using a timer.
if (Settings.getPollDelay() > 0) {
@@ -539,10 +600,18 @@ class HomeAssistantApp extends Application.AppBase {
}
}
//! Return the timer used to quit the application.
//!
//! @return Timer object
//
function getQuitTimer() as QuitTimer {
return mQuitTimer;
}
//! Return the glance view.
//!
//! @return The glance view
//
function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
mIsGlance = true;
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
@@ -554,30 +623,38 @@ class HomeAssistantApp extends Application.AppBase {
return [new HomeAssistantGlanceView(self)];
}
// Required for the Glance update timer.
//! Update the menu and API statuses. Required for the Glance update timer.
//
function updateStatus() as Void {
mGlanceTimer = null;
fetchMenuConfig();
fetchApiStatus();
}
//! Code for when the application settings are updated.
//
function onSettingsChanged() as Void {
// System.println("HomeAssistantApp onSettingsChanged()");
Settings.update();
}
// Called each time the Registered Temporal Event is to be invoked. So the object is created each time on request and
// then destroyed on completion (to save resources).
//! Called each time the Registered Temporal Event is to be invoked. So the object is created each time
//! on request and then destroyed on completion (to save resources).
//
function getServiceDelegate() as [ System.ServiceDelegate ] {
return [new BackgroundServiceDelegate()];
}
//! Determine is we are a glance or the full application. Glances should be considered to be separate applications.
//
function getIsApp() as Lang.Boolean {
return mIsApp;
}
}
//! Global function to return the application object.
//
(:glance, :background)
function getApp() as HomeAssistantApp {
return Application.getApp() as HomeAssistantApp;

View File

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

View File

@@ -11,17 +11,14 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 23 November 2023
//
//
// Description:
//
// Glance view for GarminHomeAssistant
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Glance view for GarminHomeAssistant
//
(:glance)
class HomeAssistantGlanceView extends WatchUi.GlanceView {
private static const scLeftMargin = 5; // in pixels
@@ -34,6 +31,8 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
private var mMenuStatus as WatchUi.Text or Null;
private var mAntiAlias as Lang.Boolean = false;
//! Class Constructor
//
function initialize(app as HomeAssistantApp) {
GlanceView.initialize();
mApp = app;
@@ -42,6 +41,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
}
}
//! Construct the view.
//!
//! @param dc Device context
//
function onLayout(dc as Graphics.Dc) as Void {
var h = dc.getHeight();
var tw = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String, Graphics.FONT_XTINY);
@@ -89,6 +92,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
});
}
//! Update the view with the latest status text.
//!
//! @param dc Device context
//
function onUpdate(dc as Graphics.Dc) as Void {
GlanceView.onUpdate(dc);
if(mAntiAlias) {

View File

@@ -11,20 +11,19 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
//
//
// Description:
//
// Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
// a Home Assistant Template.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
//! a Home Assistant Template.
//
class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
private var mMenu as HomeAssistantView;
//! Class Constructor
//
function initialize(
definition as Lang.Dictionary,
template as Lang.String,
@@ -48,6 +47,8 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
mMenu = new HomeAssistantView(definition, null);
}
//! Return the submenu for this group menu item.
//
function getMenuView() as HomeAssistantView {
return mMenu;
}

View File

@@ -11,20 +11,23 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
//
//
// Description:
//
// Generic menu button with an icon that optionally renders a Home Assistant Template.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Generic menu button with an icon that optionally renders a Home Assistant Template.
//
class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String or Null;
//! Class Constructor
//!
//! @param label Menu item label
//! @param template Menu item template
//! @param options Menu item options to be passed on.
//
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
@@ -43,14 +46,27 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
mTemplate = template;
}
//! Does this menu item use a template?
//!
//! @return True if the menu has a defined template else false.
//
function hasTemplate() as Lang.Boolean {
return mTemplate != null;
}
function buildTemplate() as Lang.String or Null {
//! Return the menu item's template.
//!
//! @return A string with the menu item's template definition.
//
function getTemplate() as Lang.String or Null {
return mTemplate;
}
//! Update the menu item's sub label to display the template rendered by Home Assistant.
//!
//! @param data The rendered template (typically a string) to be placed in the sub label. This may
//! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such.
//
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);

View File

@@ -11,17 +11,14 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 17 November 2023
//
//
// Description:
//
// MenuItems Factory.
//
//-----------------------------------------------------------------------------------
using Toybox.Application;
using Toybox.Lang;
using Toybox.WatchUi;
//! MenuItems Factory class.
//
class HomeAssistantMenuItemFactory {
private var mMenuItemOptions as Lang.Dictionary;
private var mTapTypeIcon as WatchUi.Bitmap;
@@ -31,6 +28,8 @@ class HomeAssistantMenuItemFactory {
private static var instance;
//! Class Constructor
//
private function initialize() {
mMenuItemOptions = {
:alignment => Settings.getMenuAlignment()
@@ -57,6 +56,8 @@ class HomeAssistantMenuItemFactory {
mHomeAssistantService = new HomeAssistantService();
}
//! Create the one and only instance of this class.
//
static function create() as HomeAssistantMenuItemFactory {
if (instance == null) {
instance = new HomeAssistantMenuItemFactory();
@@ -64,6 +65,14 @@ class HomeAssistantMenuItemFactory {
return instance;
}
//! Toggle menu item.
//!
//! @param label Menu item label.
//! @param entity_id Home Assistant Entity ID (optional)
//! @param template Template for Home Assistant to render (optional)
//! @param confirm Should this menu item selection be confirmed?
//! @param pin Should this menu item selection request the security PIN?
//
function toggle(
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
@@ -81,20 +90,30 @@ class HomeAssistantMenuItemFactory {
);
}
//! Tap menu item.
//!
//! @param label Menu item label.
//! @param entity_id Home Assistant Entity ID (optional)
//! @param template Template for Home Assistant to render (optional)
//! @param service Template for Home Assistant to render (optional)
//! @param confirm Should this menu item selection be confirmed?
//! @param pin Should this menu item selection request the security PIN?
//! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
//
function tap(
label as Lang.String or Lang.Symbol,
entity as Lang.String or Null,
entity_id as Lang.String or Null,
template as Lang.String or Null,
service as Lang.String or Null,
confirm as Lang.Boolean,
pin as Lang.Boolean,
data as Lang.Dictionary or Null
) as WatchUi.MenuItem {
if (entity != null) {
if (entity_id != null) {
if (data == null) {
data = { "entity_id" => entity };
data = { "entity_id" => entity_id };
} else {
data.put("entity_id", entity);
data.put("entity_id", entity_id);
}
}
if (service != null) {
@@ -124,6 +143,11 @@ class HomeAssistantMenuItemFactory {
}
}
//! Group menu item.
//!
//! @param definition Items array from the JSON that defines this sub menu.
//! @param template Template for Home Assistant to render (optional)
//
function group(
definition as Lang.Dictionary,
template as Lang.String or Null

View File

@@ -11,25 +11,28 @@
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
//
//
// Description:
//
// Pin Confirmation dialog and logic.
//
//-----------------------------------------------------------------------------------
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
import Toybox.Timer;
import Toybox.Attention;
import Toybox.Time;
using Toybox.Graphics;
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Timer;
using Toybox.Attention;
using Toybox.Time;
//! Pin digit used for number 0..9
//
class PinDigit extends WatchUi.Selectable {
private var mDigit as Number;
private var mDigit as Lang.Number;
function initialize(digit as Number, stepX as Number, stepY as Number) {
//! Class Constructor
//!
//! @param digit The digit this instance of the class represents and to display.
//! @param stepX Horizontal spacing.
//! @param stepY Vertical spacing.
//
function initialize(digit as Lang.Number, stepX as Lang.Number, stepY as Lang.Number) {
var marginX = stepX * 0.05; // 5% margin on all sides
var marginY = stepY * 0.05;
var x = (digit == 0) ? stepX : stepX * ((digit+2) % 3); // layout '0' in 2nd col, others ltr in 3 columns
@@ -63,24 +66,40 @@ class PinDigit extends WatchUi.Selectable {
});
mDigit = digit;
}
function getDigit() as Number {
//! Return the digit 0..9 represented by this button
//
function getDigit() as Lang.Number {
return mDigit;
}
//! Customised drawing of a PIN digit's button.
//
class PinDigitButton extends WatchUi.Drawable {
private var mText as Number;
private var mTouched as Boolean = false;
private var mText as Lang.Number;
private var mTouched as Lang.Boolean = false;
//! Class Constructor
//!
//! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.<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) {
Drawable.initialize(options);
mText = options.get(:label);
mTouched = options.get(:touched);
}
function draw(dc) {
//! Draw the PIN digit button.
//!
//! @param dc Device context
//
function draw(dc as Graphics.Dc) {
if (mTouched) {
dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE);
} else {
@@ -98,17 +117,27 @@ class PinDigit extends WatchUi.Selectable {
}
//! Pin Confirmation dialog and logic.
//
class HomeAssistantPinConfirmationView extends WatchUi.View {
static const MARGIN_X = 20; // margin on left & right side of screen (overall prettier and works better on round displays)
var mPinMask as String = "";
//! Margin on left & right side of screen (overall prettier and works better on round displays)
static const MARGIN_X = 20;
//! Indicates how many digits have been entered so far.
var mPinMask as Lang.String = "";
//! Class Constructor
//
function initialize() {
View.initialize();
}
function onLayout(dc as Dc) as Void {
//! Construct the view.
//!
//! @param dc Device context
//
function onLayout(dc as Graphics.Dc) as Void {
var stepX = (dc.getWidth() - MARGIN_X * 2) / 3; // three columns
var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry)
var digits = [];
@@ -119,7 +148,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
setLayout(digits);
}
function onUpdate(dc as Dc) as Void {
//! Update the view.
//!
//! @param dc Device context
//
function onUpdate(dc as Graphics.Dc) as Void {
View.onUpdate(dc);
if (mPinMask.length() != 0) {
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
@@ -127,7 +160,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
}
}
function updatePinMask(length as Number) {
//! Update the PIN mask displayed.
//!
//! @param length Number of `*` characters to use for the mask string.
//
function updatePinMask(length as Lang.Number) {
mPinMask = "";
for (var i=0; i<length; i++) {
mPinMask += "*";
@@ -138,17 +175,31 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
}
//! Delegate for the HomeAssistantPinConfirmationView.
//
class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
private var mPin as String;
private var mEnteredPin as String;
private var mPin as Lang.String;
private var mEnteredPin as Lang.String;
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
private var mTimer as Timer.Timer or Null;
private var mState as Lang.Boolean;
private var mFailures as PinFailures;
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();
mFailures = new PinFailures();
if (mFailures.isLocked()) {
@@ -165,7 +216,12 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
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()) {
goBack();
}
@@ -173,7 +229,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) {
mEnteredPin += instance.getDigit();
createUserFeedback();
// System.println("HomeAssitantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
// System.println("HomeAssistantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
if (mEnteredPin.length() == mPin.length()) {
if (mEnteredPin.equals(mPin)) {
mFailures.reset();
@@ -193,6 +249,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
return true;
}
//! Hepatic feedback.
//
function createUserFeedback() {
if (Attention has :vibrate && Settings.getVibrate()) {
Attention.vibrate([new Attention.VibeProfile(25, 25)]);
@@ -200,6 +258,9 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
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() {
var timeout = Settings.getConfirmTimeout(); // ms
if (timeout > 0) {
@@ -212,6 +273,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
}
}
//! Cancel PIN entry.
//
function goBack() as Void {
if (mTimer != null) {
mTimer.stop();
@@ -219,6 +282,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
WatchUi.popView(WatchUi.SLIDE_RIGHT);
}
//! Hepatic feedback for a wrong PIN and cancel entry.
//
function error() as Void {
// System.println("HomeAssistantPinConfirmationDelegate error() Wrong PIN entered");
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 {
const STORAGE_KEY_FAILURES as String = "pin_failures";
const STORAGE_KEY_LOCKED as String = "pin_locked";
const STORAGE_KEY_FAILURES as Lang.String = "pin_failures";
const STORAGE_KEY_LOCKED as Lang.String = "pin_locked";
private var mFailures as Array<Number>;
private var mLockedUntil as Number or Null;
private var mFailures as Lang.Array<Lang.Number>;
private var mLockedUntil as Lang.Number or Null;
//! Class Constructor
//
function initialize() {
// System.println("PinFailures initialize() Initializing PIN failures from storage");
var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES);
@@ -256,6 +326,8 @@ class PinFailures {
mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED);
}
//! Record a PIN entry failure. If too many have occurred lock the application.
//
function addFailure() {
mFailures.add(Time.now().value());
// System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded");
@@ -268,7 +340,7 @@ class PinFailures {
mFailures = mFailures.slice(1, null);
} else {
mFailures = [];
mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Gregorian.SECONDS_PER_MINUTE)).value();
mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Time.Gregorian.SECONDS_PER_MINUTE)).value();
Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil);
// System.println("PinFailures addFailure() Locked until " + mLockedUntil);
}
@@ -276,6 +348,9 @@ class PinFailures {
Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures);
}
//! Clear the record of previous PIN entry failures, e.g. because the correct PIN has now been entered
//! within tolerance.
//
function reset() {
// System.println("PinFailures reset() Resetting failures");
mFailures = [];
@@ -284,11 +359,18 @@ class PinFailures {
Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
}
function getLockedUntilSeconds() as Number {
//! Retrieve the remaining time the application must be locked out for.
//
function getLockedUntilSeconds() as Lang.Number {
return new Time.Moment(mLockedUntil).subtract(Time.now()).value();
}
function isLocked() as Boolean {
//! Is the application currently locked out? If the application is no longer locked out, then clear the
//! stored values used to determine this state.
//!
//! @return Boolean indicating if the application is currently locked out.
//
function isLocked() as Lang.Boolean {
if (mLockedUntil == null) {
return false;
}

View File

@@ -11,11 +11,6 @@
//
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
//
//
// Description:
//
// Calling a Home Assistant Service.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -23,10 +18,14 @@ using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.Application.Properties;
//! Calling a Home Assistant Service.
//
class HomeAssistantService {
private var mHasToast as Lang.Boolean = false;
private var mHasVibrate as Lang.Boolean = false;
//! Class Constructor
//
function initialize() {
if (WatchUi has :showToast) {
mHasToast = true;
@@ -36,7 +35,11 @@ class HomeAssistantService {
}
}
// Callback function after completing the POST request to call a service.
//! Callback function after completing the POST request to call a service.
//!
//! @param responseCode Response code.
//! @param data Response data.
//! @param context An `entity_id` supplied in the GET request `options` `Lang.Dictionary` `context` field.
//
function onReturnCall(
responseCode as Lang.Number,
@@ -107,6 +110,11 @@ class HomeAssistantService {
}
}
//! Invoke a service call for a menu item.
//!
//! @param service The Home Assistant service to be run, e.g. from the JSON `service` field.
//! @param data Data to be supplied to the service call.
//
function call(
service as Lang.String,
data as Lang.Dictionary or Null

View File

@@ -11,17 +11,14 @@
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
//
//
// Description:
//
// Menu button that triggers a service.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Menu button that triggers a service.
//
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String or Null;
@@ -29,6 +26,19 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
//! Class Constructor
//!
//! @param label Menu item label.
//! @param template Menu item template.
//! @param service Menu item service.
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security?
//! @param data Data to supply to the service call.
//! @param icon Icon to use for the menu item.
//! @param options Menu item options to be passed on.
//! @param haService Shared Home Assistant service object that will perform the required call. Only
//! one of these objects is created for all menu items to re-use.
//
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
@@ -62,6 +72,8 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
mData = data;
}
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
//
function callService() as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) {
@@ -85,7 +97,10 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
}
}
// NB. Parameter 'b' is ignored
//! Callback function after the menu items selection has been (optionally) confirmed.
//!
//! @param b Ignored. It is included in order to match the expected function prototype of the callback method.
//
function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) {
mHomeAssistantService.call(mService, mData);

View File

@@ -11,11 +11,6 @@
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
//
//
// Description:
//
// Light or switch toggle button that calls the API to maintain the up to date state.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -24,6 +19,8 @@ using Toybox.Graphics;
using Toybox.Application.Properties;
using Toybox.Timer;
//! Light or switch toggle menu button that calls the API to maintain the up to date state.
//
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mConfirm as Lang.Boolean;
private var mPin as Lang.Boolean;
@@ -31,6 +28,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mTemplate as Lang.String;
private var mHasVibrate as Lang.Boolean = false;
//! Class Constructor
//!
//! @param label Menu item label.
//! @param template Menu item template.
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security?
//! @param data Data to supply to the service call.
//! @param options Menu item options to be passed on.
//
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
@@ -58,6 +64,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
mTemplate = template;
}
//! Set the state of a toggle menu item.
//
private function setUiToggle(state as Null or Lang.String) as Void {
if (state != null) {
if (state.equals("on") && !isEnabled()) {
@@ -68,13 +76,26 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
}
function buildTemplate() as Lang.String or Null {
//! Return the menu item's template.
//!
//! @return A string with the menu item's template definition.
//
function getTemplate() as Lang.String or Null {
return mTemplate;
}
function buildToggleTemplate() as Lang.String or Null {
//! Return a toggle menu item's state template.
//!
//! @return A string with the menu item's template definition.
//
function getToggleTemplate() as Lang.String or Null {
return "{{states('" + mData.get("entity_id") + "')}}";
}
//! Update the menu item's label from a recent GET request.
//!
//! @param data This should be a string, but the way the GET response is parsed, it can also be a number.
//
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
if (data == null) {
setSubLabel(null);
@@ -100,6 +121,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
WatchUi.requestUpdate();
}
//! Update the menu item's toggle state from a recent GET request.
//!
//! @param data This should be a string of either "on" or "off".
//
function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setUiToggle("off");
@@ -126,9 +151,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
WatchUi.requestUpdate();
}
// Callback function after completing the POST request to set the status.
//! Callback function after completing the POST request to set the status.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
function onReturnSetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
function onReturnSetState(
responseCode as Lang.Number,
data as Null or Lang.Dictionary or Lang.String
) as Void {
// System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Code: " + responseCode);
// System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Data: " + data);
@@ -183,6 +214,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
getApp().setApiStatus(status);
}
//! Set the state of the toggle menu item.
//!
//! @param s Boolean indicating the desired state of the toggle switch.
//
function setState(s as Lang.Boolean) as Void {
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
setEnabled(!isEnabled());
@@ -228,6 +263,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
}
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
//
function callService(b as Lang.Boolean) as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) {
@@ -251,6 +288,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
}
//! Callback function to toggle state of this item after (optional) confirmation.
//!
//! @param b Desired toggle button state.
//
function onConfirm(b as Lang.Boolean) as Void {
setState(b);
}

View File

@@ -11,11 +11,6 @@
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
//
//
// Description:
//
// Home Assistant menu construction.
//
//-----------------------------------------------------------------------------------
using Toybox.Application;
@@ -24,8 +19,12 @@ using Toybox.Graphics;
using Toybox.System;
using Toybox.WatchUi;
//! Home Assistant menu construction.
//
class HomeAssistantView extends WatchUi.Menu2 {
//! Class Constructor
//
function initialize(
definition as Lang.Dictionary,
options as {
@@ -75,7 +74,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
}
}
// Lang.Array.addAll() fails structural type checking without including "Null" in the return type
//! Return a list of items that need to be updated within this menu structure.
//!
//! MN. Lang.Array.addAll() fails structural type checking without including "Null" in the return type
//!
//! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state.
//
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> {
var fullList = [];
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
@@ -102,26 +106,35 @@ class HomeAssistantView extends WatchUi.Menu2 {
return fullList;
}
// Called when this View is brought to the foreground. Restore
// the state of this View and prepare it to be shown. This includes
// loading resources into memory.
//! Called when this View is brought to the foreground. Restore
//! the state of this View and prepare it to be shown. This includes
//! loading resources into memory.
function onShow() as Void {}
}
//
// Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
//! Delegate for the HomeAssistantView.
//!
//! Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
//
class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
private var mIsRootMenuView as Lang.Boolean = false;
private var mTimer as QuitTimer;
//! Class Constructor
//!
//! @param isRootMenuView As menus can be nested, this state marks the top level menu so that the
//! back event can exit the application completely rather than just popping
//! a menu view.
//
function initialize(isRootMenuView as Lang.Boolean) {
Menu2InputDelegate.initialize();
mIsRootMenuView = isRootMenuView;
mTimer = getApp().getQuitTimer();
}
//! Back button event
//
function onBack() {
mTimer.reset();
@@ -135,16 +148,22 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
WatchUi.popView(WatchUi.SLIDE_RIGHT);
}
// Only for CheckboxMenu
//! Only for CheckboxMenu
//
function onDone() {
mTimer.reset();
}
// Only for CustomMenu
//! Only for CustomMenu
//
function onFooter() {
mTimer.reset();
}
//! Select event
//!
//! @param item Selected menu item.
//
function onSelect(item as WatchUi.MenuItem) as Void {
mTimer.reset();
if (item instanceof HomeAssistantToggleMenuItem) {
@@ -164,7 +183,8 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
}
}
// Only for CustomMenu
//! Only for CustomMenu
//
function onTitle() {
mTimer.reset();
}

View File

@@ -11,11 +11,6 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// Quit the application after a period of inactivity in order to save the battery.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -23,18 +18,27 @@ using Toybox.Timer;
using Toybox.Application.Properties;
using Toybox.WatchUi;
//! Quit the application after a period of inactivity in order to save the battery.
//!
class QuitTimer extends Timer.Timer {
//! Class Constructor
//
function initialize() {
Timer.Timer.initialize();
}
//! Can't see how to make a method object from `System.exit()` without this layer of
//! indirection. I assume this is because `System` is a static class.
//
function exitApp() as Void {
// System.println("QuitTimer exitApp(): Exiting");
// This will exit the system cleanly from any point within an app.
System.exit();
}
//! Kick off the quit timer.
//
function begin() {
var api_timeout = Settings.getAppTimeout(); // ms
if (api_timeout > 0) {
@@ -42,6 +46,8 @@ class QuitTimer extends Timer.Timer {
}
}
//! Reset the quit timer.
//
function reset() {
// System.println("QuitTimer reset(): Restarted quit timer");
stop();

View File

@@ -11,35 +11,36 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// A view with added methods to scale from percentages of scrren size to pixels.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Math;
//! A view that provides a common method 'pixelsForScreen' to make Views easier to layout on different
//! sized watch screens.
//
class ScalableView extends WatchUi.View {
//! Retain the local screen width for efficiency
private var mScreenWidth;
//! Class Constructor
//
function initialize() {
View.initialize();
mScreenWidth = System.getDeviceSettings().screenWidth;
}
// Convert a fraction expressed as a percentage (%) to a number of pixels for the
// screen's dimensions.
//
// Parameters:
// * dc - Device context
// * pc - Percentage (%) expressed as a number in the range 0.0..100.0
//
// Uses screen width rather than screen height as rectangular screens tend to have
// height > width.
//
//! Convert a fraction expressed as a percentage (%) to a number of pixels for the
//! screen's dimensions.
//!
//! Uses screen width rather than screen height as rectangular screens tend to have
//! height > width.
//!
//! @param pc Percentage (%) expressed as a number in the range 0.0..100.0
//!
//! @return Number of pixels for the screen's dimensions for a fraction expressed as a percentage (%).
//!
function pixelsForScreen(pc as Lang.Float) as Lang.Number {
return Math.round(pc * mScreenWidth) / 100;
}

View File

@@ -11,16 +11,6 @@
//
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 23 November 2023
//
//
// Description:
//
// Home Assistant settings.
//
// WARNING!
//
// Careful putting ErrorView.show() calls in here. They need to be guarded so that
// they do not get called when only displaying the glance view.
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -31,6 +21,11 @@ using Toybox.System;
using Toybox.Background;
using Toybox.Time;
//! Home Assistant settings.
//!
//! <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)
class Settings {
private static var mApiKey as Lang.String = "";
@@ -40,19 +35,24 @@ class Settings {
private static var mCacheConfig as Lang.Boolean = false;
private static var mClearCache as Lang.Boolean = false;
private static var mVibrate as Lang.Boolean = false;
private static var mAppTimeout as Lang.Number = 0; // seconds
private static var mPollDelay as Lang.Number = 0; // seconds
private static var mConfirmTimeout as Lang.Number = 3; // seconds
//! seconds
private static var mAppTimeout as Lang.Number = 0;
//! seconds
private static var mPollDelay as Lang.Number = 0;
//! seconds
private static var mConfirmTimeout as Lang.Number = 3;
private static var mPin as Lang.String or Null = "0000";
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
private static var mIsSensorsLevelEnabled as Lang.Boolean = false;
private static var mBatteryRefreshRate as Lang.Number = 15; // minutes
//! minutes
private static var mBatteryRefreshRate as Lang.Number = 15;
private static var mIsApp as Lang.Boolean = false;
private static var mHasService as Lang.Boolean = false;
// Must keep the object so it doesn't get garbage collected.
//! Must keep the object so it doesn't get garbage collected.
private static var mWebhookManager as WebhookManager or Null;
// Called on application start and then whenever the settings are changed.
//! Called on application start and then whenever the settings are changed.
//
static function update() {
mIsApp = getApp().getIsApp();
mApiKey = Properties.getValue("api_key");
@@ -71,6 +71,8 @@ class Settings {
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
}
//! A webhook is required for non-privileged API calls.
//
static function webhook() {
if (System has :ServiceDelegate) {
mHasService = true;
@@ -116,65 +118,123 @@ class Settings {
// }
}
//! Get the API key supplied as part of the Settings.
//!
//! @return The API Key
//
static function getApiKey() as Lang.String {
return mApiKey;
}
//! Get the Webhook ID supplied as part of the Settings.
//!
//! @return The Webhook ID
//
static function getWebhookId() as Lang.String {
return mWebhookId;
}
//! Set the Webhook ID supplied as part of the Settings.
//!
//! @param webhookId The Webhook ID value to be saved.
//
static function setWebhookId(webhookId as Lang.String) {
mWebhookId = webhookId;
Properties.setValue("webhook_id", mWebhookId);
}
//! Delete the Webhook ID saved as part of the Settings.
//
static function unsetWebhookId() {
mWebhookId = "";
Properties.setValue("webhook_id", mWebhookId);
}
//! Get the API URL supplied as part of the Settings.
//!
//! @return The API URL
//
static function getApiUrl() as Lang.String {
return mApiUrl;
}
//! Get the menu configuration URL supplied as part of the Settings.
//!
//! @return The menu configuration URL
//
static function getConfigUrl() as Lang.String {
return mConfigUrl;
}
//! Get the menu cache Boolean option supplied as part of the Settings.
//!
//! @return Boolean for whether the menu should be cached to save application
//! start up time.
//
static function getCacheConfig() as Lang.Boolean {
return mCacheConfig;
}
//! Get the clear cache Boolean option supplied as part of the Settings.
//!
//! @return Boolean for whether the cache should be cleared next time the
//! application is started, forcing a menu refresh.
//
static function getClearCache() as Lang.Boolean {
return mClearCache;
}
//! Unset the clear cache Boolean option supplied as part of the Settings.
//
static function unsetClearCache() {
mClearCache = false;
Properties.setValue("clear_cache", mClearCache);
}
//! Get the vibration Boolean option supplied as part of the Settings.
//!
//! @return Boolean for whether vibration is enabled.
//
static function getVibrate() as Lang.Boolean {
return mVibrate;
}
//! Get the application timeout value supplied as part of the Settings.
//!
//! @return The application timeout in milliseconds.
//
static function getAppTimeout() as Lang.Number {
return mAppTimeout * 1000; // Convert to milliseconds
}
//! Get the application API polling interval supplied as part of the Settings.
//!
//! @return The application API polling interval in milliseconds.
//
static function getPollDelay() as Lang.Number {
return mPollDelay * 1000; // Convert to milliseconds
}
//! Get the menu item confirmation delay supplied as part of the Settings.
//!
//! @return The menu item confirmation delay in milliseconds.
//
static function getConfirmTimeout() as Lang.Number {
return mConfirmTimeout * 1000; // Convert to milliseconds
}
//! Get the menu item security PIN supplied as part of the Settings.
//!
//! @return The menu item security PIN.
//
static function getPin() as Lang.String or Null {
return mPin;
}
//! Check the user selected PIN confirms to 4 digits as a string.
//!
//! @return The validated 4 digit string.
//
private static function validatePin() as Lang.String or Null {
var pin = Properties.getValue("pin");
if (pin.toNumber() == null || pin.length() != 4) {
@@ -183,14 +243,24 @@ class Settings {
return pin;
}
//! Get the menu item alignment as part of the Settings.
//!
//! @return The menu item alignment.
//
static function getMenuAlignment() as Lang.Number {
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
}
//! Is logging of the watch sensors enabled? E.g. battery, activity etc.
//!
//! @return Boolean for whether logging of the watch sensors is enabled.
//
static function isSensorsLevelEnabled() as Lang.Boolean {
return mIsSensorsLevelEnabled;
}
//! Disable logging of the watch's sensors.
//
static function unsetIsSensorsLevelEnabled() {
mIsSensorsLevelEnabled = false;
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);

View File

@@ -11,88 +11,99 @@
//
// J D Abbey & P A Abbey, 28 December 2022
//
//
// Description:
//
// WebLog provides a logging and hence debugging aid for when the application is
// deployed to the watch. This is only used for development and use of it must not
// persist into a deployed version. It uses a string buffer to group log entries into
// larger submissions in order to prevent overflow of the blue tooth stack.
//
//-----------------------------------------------------------------------------------
//
// Usage:
// wl = new WebLog();
// wl.clear();
// wl.println("Debug Message");
// wl.flush();
//
// https://domain.name/path/log.php
//
// <?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.Lang;
using Toybox.System;
//! WebLog provides a logging and hence debugging aid for when the application is
//! deployed to the watch. This is only used for development and use of it must not
//! persist into a deployed version. It uses a string buffer to group log entries into
//! larger submissions in order to prevent overflow of the Bluetooth stack.
//!
//! Usage:
//! <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)
class WebLog {
private var callsbuffer = 4 as Lang.Number;
private var callsBuffer = 4 as Lang.Number;
private var numCalls = 0 as Lang.Number;
private var buffer = "" as Lang.String;
// Set the number of calls to print() before sending the buffer to the online
// logger.
//! Set the number of calls to print() before sending the buffer to the online
//! logger.
//!
//! @param l The number of log calls to buffer before writing to the online service.
//
function setCallsBuffer(l as Lang.Number) {
callsbuffer = l;
callsBuffer = l;
}
// Get the number of calls to print() before sending the buffer to the online
// logger.
//! Get the number of calls to print() before sending the buffer to the online
//! logger.
//!
//! @return The number of log calls to buffer before writing to the online service.
//
function getCallsBuffer() as Lang.Number {
return callsbuffer;
return callsBuffer;
}
// Create a debug log over the Internet to keep track of the watch's runtime
// execution.
//! Create a debug log over the Internet to keep track of the watch's runtime
//! execution.
//!
//! @param str The string to log.
//
function print(str as Lang.String) {
var myTime = System.getClockTime();
buffer += myTime.hour.format("%02d") + ":" + myTime.min.format("%02d") + ":" + myTime.sec.format("%02d") + " " + str;
numCalls++;
// System.println("WebLog print() str = " + str);
if (numCalls >= callsbuffer) {
if (numCalls >= callsBuffer) {
doPrint();
}
}
// Create a debug log over the Internet to keep track of the watch's runtime
// execution. Add a new line character to the end.
//! Create a debug log over the Internet to keep track of the watch's runtime
//! execution. Add a new line character to the end.
//!
//! @param str The string to log.
//
function println(str as Lang.String) {
print(str + "\n");
}
// Flush the current buffer to the online logger even if it has not reach the
// submission level set by 'callsbuffer'.
//! Flush the current buffer to the online logger even if it has not reach the
//! submission level set by 'callsBuffer'.
//
function flush() {
// System.println("WebLog flush()");
@@ -101,7 +112,7 @@ class WebLog {
}
}
// Perform the submission to the online logger.
//! Perform the submission to the online logger.
//
function doPrint() {
// System.println("WebLog doPrint()");
@@ -122,8 +133,8 @@ class WebLog {
buffer = "";
}
// Clear the debug log over the Internet to start a new track of the watch's runtime
// execution.
//! Clear the debug log over the Internet to start a new track of the watch's runtime
//! execution.
//
function clear() {
// System.println("WebLog clear()");
@@ -143,7 +154,10 @@ class WebLog {
buffer = "";
}
// Callback function to print the outcome of a doPrint() method.
//! Callback function to print the outcome of a doPrint() method. Typically used for debugging this class.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
function onLog(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// if (responseCode != 200) {
@@ -153,7 +167,10 @@ class WebLog {
// }
}
// Callback function to print the outcome of a clear() method.
// Callback function to print the outcome of a clear() method. Typically used for debugging this class.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
function onClear(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// if (responseCode != 200) {

View File

@@ -11,14 +11,6 @@
//
// P A Abbey & J D Abbey, 10 January 2024
//
//
// Description:
//
// Home Assistant Webhook creation.
//
// Reference:
// * https://developers.home-assistant.io/docs/api/native-app-integration
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
@@ -26,10 +18,24 @@ using Toybox.Communications;
using Toybox.System;
using Toybox.WatchUi;
// Can use push view so must never be run in a glance context
//! Home Assistant Webhook creation.
//!
//! NB. Because we can use push view (E.g. `ErrorView.show()`) this class must never
//! be run in a glance context.
//!
//! Reference: https://developers.home-assistant.io/docs/api/native-app-integration
//
class WebhookManager {
function onReturnRequestWebhookId(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
//! Callback for requesting a Webhook ID.
//!
//! @param responseCode Response code
//! @param data Return data
//
function onReturnRequestWebhookId(
responseCode as Lang.Number,
data as Null or Lang.Dictionary or Lang.String
) as Void {
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
@@ -84,6 +90,8 @@ class WebhookManager {
}
}
//! Request a Webhook ID from Home Assistant for use in this application.
//
function requestWebhookId() {
var deviceSettings = System.getDeviceSettings();
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
@@ -115,7 +123,18 @@ class WebhookManager {
);
}
function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, sensors as Lang.Array<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) {
case Communications.BLE_HOST_TIMEOUT:
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();
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
// System.println("WebhookManager registerWebhookSensor(): URL=" + url);
@@ -217,6 +240,8 @@ class WebhookManager {
);
}
//! Request the creation of all the supported watch sensors on the Home Assistant instance.
//
function registerWebhookSensors() {
var heartRate = Activity.getActivityInfo().currentHeartRate;