diff --git a/README.md b/README.md
index 2a9c705..05d6aaa 100644
--- a/README.md
+++ b/README.md
@@ -131,7 +131,8 @@ Example schema:
"name": "TV Lights Scene",
"type": "tap",
"tap_action": {
- "service": "scene.turn_on"
+ "service": "scene.turn_on",
+ "pin": true
}
}
]
@@ -264,6 +265,8 @@ The application timeout prevents the HomeAssistant App running on your watch whe
There is a second timeout value for confirmation views. This is intended for use with more sensitive toggles so that the confirmation view is not left open and forgotten and then confirmed accidentally without you noticing. **We cannot advise you this is safe, be careful what you toggle with the watch application!**
+The confirmation timeout is also used for the maximum time between clicks in the PIN confirmation dialog. The PIN confirmation provides a more secure alternative for toggling security-sensitive actions.
+
There is a toggle setting for "text alignment" that provides finer adjustment for right-to-left languages. Perhaps this could be made automatic based on device language?
The application and widget both include a background service to report your watch's battery level and charging status. You may enable a background service to report the battery level to your Home Assistant. This is not available over your Bluetooth connection like with other Bluetooth devices as Garmin did not implement it. This no longer requires any setup, and we offer this [trouble shooting](TroubleShooting.md#watch-battery-level-reporting) guide. The last field here is readonly and allows the user to copy & paste the Webhook ID setup by the application when required for this trouble shooting guide.
diff --git a/config.schema.json b/config.schema.json
index 0f4d6ec..1e70bba 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -37,6 +37,9 @@
"properties": {
"confirm": {
"$ref": "#/$defs/confirm"
+ },
+ "pin": {
+ "$ref": "#/$defs/pin"
}
},
"additionalProperties": false
@@ -213,6 +216,9 @@
"confirm": {
"$ref": "#/$defs/confirm"
},
+ "pin": {
+ "$ref": "#/$defs/pin"
+ },
"data": {
"type": "object",
"title": "Your services's parameters",
@@ -230,6 +236,12 @@
"default": false,
"title": "Confirmation",
"description": "Optional confirmation of the action before execution as a precaution."
+ },
+ "pin": {
+ "type": "boolean",
+ "default": false,
+ "title": "PIN Confirmation",
+ "description": "Optional PIN confirmation of the action before execution as a precaution. Has precedence over 'confirm': true if both are set."
}
}
}
diff --git a/examples/Actions.md b/examples/Actions.md
index 3d840c0..d2c6527 100644
--- a/examples/Actions.md
+++ b/examples/Actions.md
@@ -38,6 +38,22 @@ For example:
}
```
+**The authors do not advise the use of this application for security sensitive devices. But we suspect users are taking that risk anyway, hence a PIN confirmation is provided that can be used for additional menu item security.**
+
+This can be enabled by setting the `pin` field in the `tap_action`. The `pin` field overrides `confirm`. Explicitly setting `confirm` is not necessary.
+
+The 4-digit PIN is set globally for all actions in the app settings in Connect IQ.
+
+```json
+ "tap_action": {
+ "pin": true
+ }
+```
+
+When entering an invalid PIN for the fifth time within 2 minutes, the PIN dialog will be locked for all actions for the next 10 minutes. Entering a valid PIN will always reset the failure counter.
+
+
+
Note that for notify events, you _must_ not supply an `entity_id` or the API call will fail. There are other examples too.
```json
diff --git a/examples/Switches.md b/examples/Switches.md
index 0474499..fa22fa5 100644
--- a/examples/Switches.md
+++ b/examples/Switches.md
@@ -22,7 +22,20 @@ And with an optional confirmation:
"tap_action": {
"confirm": true
}
- },
+ }
+```
+
+or an optional PIN confirmation:
+
+```json
+ {
+ "entity": "light.exterior",
+ "name": "Exterior Lights",
+ "type": "toggle",
+ "tap_action": {
+ "pin": true
+ }
+ }
```
To support a non-standard light, switch, or automation as a toggle menu item you may like to define a custom switch. In order to facilitate custom switches at this time, you must create a template switch in HomeAssistant.
diff --git a/examples/Templates.md b/examples/Templates.md
index c6ec771..ec27d82 100644
--- a/examples/Templates.md
+++ b/examples/Templates.md
@@ -101,7 +101,11 @@ Here we also use the else clause as well to give proper text instead of just `on
> [!IMPORTANT]
> We advise users against adding security devices.
-However, users are doing this **against our advice** and asking how to operate 'covers'. This is an example of toggling a garage door open and closed with confirmation. *Do this at your own risk*.
+However, for users doing this **against our advice**, we strongly recommend to secure confirmation of the action using our PIN confirmation dialog.
+This an example of toggling a garage door open and closed with a PIN confirmation. *Do this at your own risk*.
+
+The PIN confirmation is activated for actions with `"pin": true`. The PIN is configured globally in the application settings. The PIN needs to be a 4-digit number.
+The user has 5 attempts to provide a valid PIN within 2 minutes. If too many failures have been detected during this time, the PIN dialog will be locked for 10 minutes.
Note: Only when you use the `tap_action` field do you also need to include the `entity` field. This is a change to a previous version of the application, hence the presence of the `entity` field will be ignored for backwards compatibility, and the schema will provide a warning only.
@@ -113,7 +117,7 @@ Note: Only when you use the `tap_action` field do you also need to include the `
"content": "{% if is_state('binary_sensor.garage_connected', 'on') %}{{state_translated('cover.garage_door')}} - {{state_attr('cover.garage_door', 'current_position')}}%{%else%}Unconnected{% endif %}",
"tap_action": {
"service": "cover.toggle",
- "confirm": true
+ "pin": true
}
}
```
diff --git a/images/pin_view.png b/images/pin_view.png
new file mode 100644
index 0000000..7f8158c
Binary files /dev/null and b/images/pin_view.png differ
diff --git a/resources-deu/strings/corrections.xml b/resources-deu/strings/corrections.xml
index bea1d18..4e47922 100644
--- a/resources-deu/strings/corrections.xml
+++ b/resources-deu/strings/corrections.xml
@@ -39,6 +39,8 @@
des Geräts zu schonen.
Nach dieser Zeit (in Sekunden) wird der Bestätigungsdialog einer Aktion geschlossen und die
Aktion abgebrochen. Auf 0 setzen, um den Timeout zu deaktivieren.
+ 4-stellige PIN für alle Actions mit 'confirm': true (0000-9999).
+ Bitte eine gültige 4-stellige numerische PIN in den App Einstellungen eingeben (0000-9999).
(Nur Widget) Anwendung automatisch über das Widget starten ohne drauftippen zu müssen.
Hintergrunddienst aktivieren, um den Ladezustand der Batterie an HomeAssistant zu senden.
Die Aktualisierungsrate (in Minuten) mit der der Ladezustand der Batterie
diff --git a/resources/settings/properties.xml b/resources/settings/properties.xml
index 06b7fbb..a108e2c 100644
--- a/resources/settings/properties.xml
+++ b/resources/settings/properties.xml
@@ -65,6 +65,12 @@
-->
3
+
+ 0000
+
diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml
index 346ad84..333b5ae 100644
--- a/resources/settings/settings.xml
+++ b/resources/settings/settings.xml
@@ -79,6 +79,13 @@
+
+
+
+
Empty
Template Error
Potential Error
+ PIN input locked for
+ seconds
+ Wrong PIN
Select...
@@ -53,6 +56,8 @@
Timeout in seconds. Exit the application after this period of inactivity to save the device battery.
Additional poll delay (in seconds). Adds a delay between the status update of all menu items.
After this time (in seconds), a confirmation dialog for an action is automatically closed and the action is cancelled. Set to 0 to disable the timeout.
+ 4-digit PIN to be used for all actions that require confirmation (0000-9999).
+ Please configure a valid 4-digit numeric PIN between 0000 and 9999 in the application settings.
Left (off) or Right (on) Menu Alignment.
Left to right
Right to Left
diff --git a/source/Globals.mc b/source/Globals.mc
index ebd7e35..3c40a7d 100644
--- a/source/Globals.mc
+++ b/source/Globals.mc
@@ -32,4 +32,10 @@ class Globals {
static const scApiResume = 200; // ms
// 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
+
}
diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc
index f7858cb..e86531d 100644
--- a/source/HomeAssistantApp.mc
+++ b/source/HomeAssistantApp.mc
@@ -109,6 +109,9 @@ class HomeAssistantApp extends Application.AppBase {
} else if (Settings.getConfigUrl().length() == 0) {
// System.println("HomeAssistantApp getInitialView(): No configuration URL in the application settings.");
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoConfigUrl) as Lang.String + ".");
+ } else if (Settings.getPin() == null) {
+ // System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
+ return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String + ".");
} else if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantApp getInitialView(): No Phone connection, skipping API call.");
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
diff --git a/source/HomeAssistantMenuItemFactory.mc b/source/HomeAssistantMenuItemFactory.mc
index e522820..e92ff03 100644
--- a/source/HomeAssistantMenuItemFactory.mc
+++ b/source/HomeAssistantMenuItemFactory.mc
@@ -68,12 +68,14 @@ class HomeAssistantMenuItemFactory {
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
template as Lang.String or Null,
- confirm as Lang.Boolean
+ confirm as Lang.Boolean,
+ pin as Lang.Boolean
) as WatchUi.MenuItem {
return new HomeAssistantToggleMenuItem(
label,
template,
confirm,
+ pin,
{ "entity_id" => entity_id },
mMenuItemOptions
);
@@ -85,6 +87,7 @@ class HomeAssistantMenuItemFactory {
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) {
@@ -100,6 +103,7 @@ class HomeAssistantMenuItemFactory {
template,
service,
confirm,
+ pin,
data,
mTapTypeIcon,
mMenuItemOptions,
@@ -111,6 +115,7 @@ class HomeAssistantMenuItemFactory {
template,
service,
confirm,
+ pin,
data,
mInfoTypeIcon,
mMenuItemOptions,
diff --git a/source/HomeAssistantPinConfirmation.mc b/source/HomeAssistantPinConfirmation.mc
new file mode 100644
index 0000000..830d365
--- /dev/null
+++ b/source/HomeAssistantPinConfirmation.mc
@@ -0,0 +1,303 @@
+//-----------------------------------------------------------------------------------
+//
+// Distributed under MIT Licence
+// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
+//
+//-----------------------------------------------------------------------------------
+//
+// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
+// tested on a Venu 2 device. The source code is provided at:
+// https://github.com/house-of-abbey/GarminHomeAssistant.
+//
+// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
+//
+//
+// Description:
+//
+// Pin Confirmation dialog and logic.
+//
+//-----------------------------------------------------------------------------------
+
+import Toybox.Graphics;
+import Toybox.Lang;
+import Toybox.WatchUi;
+import Toybox.Timer;
+import Toybox.Attention;
+import Toybox.Time;
+
+class PinDigit extends WatchUi.Selectable {
+
+ private var mDigit as Number;
+
+ function initialize(digit as Number, stepX as Number, stepY as 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
+ x += marginX + HomeAssistantPinConfirmationView.MARGIN_X;
+ var y = (digit == 0) ? stepY * 4 : (digit <= 3) ? stepY : (digit <=6) ? stepY * 2 : stepY * 3; // layout '0' in bottom row (5), others top to bottom in 3 rows (2-4) (row 1 is reserved for masked pin)
+ y += marginY;
+ var width = stepX - (marginX * 2);
+ var height = stepY - (marginY * 2);
+
+ var button = new PinDigitButton({
+ :width=>width,
+ :height=>height,
+ :label=>digit
+ });
+
+ var buttonTouched = new PinDigitButton({
+ :width=>width,
+ :height=>height,
+ :label=>digit,
+ :touched=>true
+ });
+
+ // initialize selectable
+ Selectable.initialize({
+ :stateDefault=>button,
+ :stateHighlighted=>buttonTouched,
+ :locX =>x,
+ :locY=>y,
+ :width=>width,
+ :height=>height
+ });
+
+ mDigit = digit;
+
+ }
+
+ function getDigit() as Number {
+ return mDigit;
+ }
+
+ class PinDigitButton extends WatchUi.Drawable {
+ private var mText as Number;
+ private var mTouched as Boolean = false;
+
+ function initialize(options) {
+ Drawable.initialize(options);
+ mText = options.get(:label);
+ mTouched = options.get(:touched);
+ }
+
+ function draw(dc) {
+ if (mTouched) {
+ dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE);
+ } else {
+ dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_DK_GRAY);
+ }
+ dc.fillCircle(locX + width / 2, locY + height / 2, height / 2); // circle fill
+ dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_LT_GRAY);
+ dc.setPenWidth(3);
+ dc.drawCircle(locX + width / 2, locY + height / 2, height / 2); // circle outline
+ dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
+ dc.drawText(locX+width / 2, locY+height / 2, Graphics.FONT_TINY, mText, Graphics.TEXT_JUSTIFY_CENTER|Graphics.TEXT_JUSTIFY_VCENTER); // center text in circle
+ }
+
+ }
+
+}
+
+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 = "";
+
+ function initialize() {
+ View.initialize();
+ }
+
+ function onLayout(dc as 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 = [];
+ for (var i=0; i<=9; i++) {
+ digits.add(new PinDigit(i, stepX, stepY));
+ }
+ // draw digits
+ setLayout(digits);
+ }
+
+ function onUpdate(dc as Dc) as Void {
+ View.onUpdate(dc);
+ if (mPinMask.length() != 0) {
+ dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
+ dc.drawText(dc.getWidth()/2, dc.getHeight()/10, Graphics.FONT_SYSTEM_SMALL, mPinMask, Graphics.TEXT_JUSTIFY_CENTER|Graphics.TEXT_JUSTIFY_VCENTER);
+ }
+ }
+
+ function updatePinMask(length as Number) {
+ mPinMask = "";
+ for (var i=0; i;
+ private var mLockedUntil as Number or Null;
+
+ function initialize() {
+ // System.println("PinFailures initialize() Initializing PIN failures from storage");
+ var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES);
+ mFailures = (failures == null) ? [] : failures;
+ mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED);
+ }
+
+ function addFailure() {
+ mFailures.add(Time.now().value());
+ // System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded");
+ if (mFailures.size() >= Globals.scPinMaxFailures) {
+ // System.println("PinFailures addFailure() Too many failures detected");
+ var oldestFailureOutdate = new Time.Moment(mFailures[0]).add(new Time.Duration(Globals.scPinMaxFailureMinutes * 60));
+ // System.println("PinFailures addFailure() Oldest failure: " + oldestFailureOutdate.value() + " Now:" + Time.now().value());
+ if (new Time.Moment(Time.now().value()).greaterThan(oldestFailureOutdate)) {
+ // System.println("PinFailures addFailure() Pruning oldest outdated failure");
+ mFailures = mFailures.slice(1, null);
+ } else {
+ mFailures = [];
+ mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Gregorian.SECONDS_PER_MINUTE)).value();
+ Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil);
+ // System.println("PinFailures addFailure() Locked until " + mLockedUntil);
+ }
+ }
+ Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures);
+ }
+
+ function reset() {
+ // System.println("PinFailures reset() Resetting failures");
+ mFailures = [];
+ mLockedUntil = null;
+ Application.Storage.deleteValue(STORAGE_KEY_FAILURES);
+ Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
+ }
+
+ function getLockedUntilSeconds() as Number {
+ return new Time.Moment(mLockedUntil).subtract(Time.now()).value();
+ }
+
+ function isLocked() as Boolean {
+ if (mLockedUntil == null) {
+ return false;
+ }
+ var isLocked = new Time.Moment(Time.now().value()).lessThan(new Time.Moment(mLockedUntil));
+ if (!isLocked) {
+ mLockedUntil = null;
+ Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
+ }
+ return isLocked;
+ }
+
+}
\ No newline at end of file
diff --git a/source/HomeAssistantTapMenuItem.mc b/source/HomeAssistantTapMenuItem.mc
index 17f861b..8d1915c 100644
--- a/source/HomeAssistantTapMenuItem.mc
+++ b/source/HomeAssistantTapMenuItem.mc
@@ -27,6 +27,7 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String;
private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean;
+ private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
function initialize(
@@ -34,6 +35,7 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
template as Lang.String,
service as Lang.String or Null,
confirm as Lang.Boolean,
+ pin as Lang.Boolean,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
@@ -53,6 +55,7 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
mTemplate = template;
mService = service;
mConfirm = confirm;
+ mPin = pin;
mData = data;
}
@@ -84,7 +87,18 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
}
function callService() as Void {
- if (mConfirm) {
+ var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
+ if (mPin && hasTouchScreen) {
+ var pin = Settings.getPin();
+ if (pin != null) {
+ var pinConfirmationView = new HomeAssistantPinConfirmationView();
+ WatchUi.pushView(
+ pinConfirmationView,
+ new HomeAssistantPinConfirmationDelegate(method(:onConfirm), false, pin, pinConfirmationView),
+ WatchUi.SLIDE_IMMEDIATE
+ );
+ }
+ } else if (mConfirm) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc
index 9ba37a8..b24ad62 100644
--- a/source/HomeAssistantToggleMenuItem.mc
+++ b/source/HomeAssistantToggleMenuItem.mc
@@ -26,6 +26,7 @@ using Toybox.Timer;
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mConfirm as Lang.Boolean;
+ private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary;
private var mTemplate as Lang.String;
private var mHasVibrate as Lang.Boolean = false;
@@ -34,6 +35,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
label as Lang.String or Lang.Symbol,
template as Lang.String,
confirm as Lang.Boolean,
+ pin as Lang.Boolean,
data as Lang.Dictionary or Null,
options as {
:alignment as WatchUi.MenuItem.Alignment,
@@ -45,6 +47,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
mHasVibrate = true;
}
mConfirm = confirm;
+ mPin = pin;
mData = data;
mTemplate = template;
}
@@ -213,14 +216,25 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
function callService(b as Lang.Boolean) as Void {
- if (mConfirm) {
+ var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
+ if (mPin && hasTouchScreen) {
+ var pin = Settings.getPin();
+ if (pin != null) {
+ var pinConfirmationView = new HomeAssistantPinConfirmationView();
+ WatchUi.pushView(
+ pinConfirmationView,
+ new HomeAssistantPinConfirmationDelegate(method(:onConfirm), b, pin, pinConfirmationView),
+ WatchUi.SLIDE_IMMEDIATE
+ );
+ }
+ } else if (mConfirm) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), b),
WatchUi.SLIDE_IMMEDIATE
);
} else {
- setState(b);
+ onConfirm(b);
}
}
@@ -228,4 +242,5 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
setState(b);
}
+
}
diff --git a/source/HomeAssistantView.mc b/source/HomeAssistantView.mc
index 1f75f32..76e0edf 100644
--- a/source/HomeAssistantView.mc
+++ b/source/HomeAssistantView.mc
@@ -51,10 +51,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
var tap_action = items[i].get("tap_action") as Lang.Dictionary or Null;
var service = items[i].get("service") as Lang.String or Null; // Deprecated schema
var confirm = false as Lang.Boolean or Null;
+ var pin = false as Lang.Boolean or Null;
var data = null as Lang.Dictionary or Null;
if (tap_action != null) {
service = tap_action.get("service");
confirm = tap_action.get("confirm"); // Optional
+ pin = tap_action.get("pin"); // Optional
data = tap_action.get("data"); // Optional
if (confirm == null) {
confirm = false;
@@ -62,9 +64,9 @@ class HomeAssistantView extends WatchUi.Menu2 {
}
if (type != null && name != null) {
if (type.equals("toggle") && entity != null) {
- addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm));
+ addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm, pin));
} else if ((type.equals("tap") && service != null) || (type.equals("template") && content != null)) {
- addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, data));
+ addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, pin, data));
} else if (type.equals("group")) {
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
}
diff --git a/source/Settings.mc b/source/Settings.mc
index 1bb7520..06191a6 100644
--- a/source/Settings.mc
+++ b/source/Settings.mc
@@ -38,6 +38,7 @@ class Settings {
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
+ 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
@@ -59,6 +60,7 @@ class Settings {
mAppTimeout = Properties.getValue("app_timeout");
mPollDelay = Properties.getValue("poll_delay_combined");
mConfirmTimeout = Properties.getValue("confirm_timeout");
+ mPin = validatePin();
mMenuAlignment = Properties.getValue("menu_alignment");
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
@@ -164,6 +166,18 @@ class Settings {
return mConfirmTimeout * 1000; // Convert to milliseconds
}
+ static function getPin() as Lang.String or Null {
+ return mPin;
+ }
+
+ private static function validatePin() as Lang.String or Null {
+ var pin = Properties.getValue("pin");
+ if (pin.toNumber() == null || pin.length() != 4) {
+ return null;
+ }
+ return pin;
+ }
+
static function getMenuAlignment() as Lang.Number {
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
}