Feature: PIN confirmation dialog (#192)

## About this PR:
Being one of those incorrigible fools who want to open and unlock stuff
using my watch, I thought of a way to improve security when calling a
HomeAssistant service from the Garmin App.

This change adds a PIN confirmation dialog that is displayed whenever a
`pin` property has been set in the `tap_action`. Because of the size of
the screen, PINs consist of a sequence of abritrary length using the
digits 1-4.

![2024-10-16 11_16_09-CIQ Simulator - Venu® Sq 2 Music (5 0
0)](https://github.com/user-attachments/assets/f6e09c00-5dde-4972-b7dc-256d4d94e99e)

Due to public visibility of the dashboard config, a pin mask can be
configured in the app's settings to transcode the PIN on the watch for
additional security. Using a cipher for PIN encryption has been
discarded due to the complexity involved in generating the encrypted
PINs for the end user.

The PIN dialog is configured to allow up to 4 erraneous PIN inputs in 2
minutes, a 5th failure will lock the PIN dialog (for all actions) for 10
minutes. Entering a correct PIN will reset the failure count.

## Changes included in this PR:
- PIN confirmation screen
- called whenever a `pin` has been configured in a `tap_action`. This
overrides the `confirm` property. Users without a touchscreen will be
routed to the regular confirmation dialog.
- optional `pin_mask` configuration setting, which will be used to
transcode the digits of the PIN (see documentation change)
	- german translation of the `pin_mask` label
	- failed attemps and PIN lock are stored in application storage
- Addition of `pin` field to the JSON Schema
- Additions to the docs

Tested on a few devices in the simulator, tested and running as a beta
on a Venu 2 Sq Music.

## Disclaimer:
This is my first attempt at a Monkey C project.
I probably learned more from looking at the existing app than from the
sdk samples. Awesome work!

I added this mainly because I wanted a feature like that for myself,
thought it is a good thing to add and because I was interested in
developing for Garmin devices.
I would be glad if you like the idea and are open to review it and
include it in the app.

I have tried to use coding style and conventions from the existing
project as far as I could see them.
This commit is contained in:
Philip Abbey
2024-11-18 19:00:05 +00:00
committed by GitHub
18 changed files with 440 additions and 10 deletions

View File

@ -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.

View File

@ -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."
}
}
}

View File

@ -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.
<img src="../images/pin_view.png" width="200" title="Confirmation View"/>
Note that for notify events, you _must_ not supply an `entity_id` or the API call will fail. There are other examples too.
```json

View File

@ -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.

View File

@ -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
}
}
```

BIN
images/pin_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -39,6 +39,8 @@
des Geräts zu schonen.</string>
<string id="SettingsConfirmTimeout">Nach dieser Zeit (in Sekunden) wird der Bestätigungsdialog einer Aktion geschlossen und die
Aktion abgebrochen. Auf 0 setzen, um den Timeout zu deaktivieren.</string>
<string id="SettingsPin">4-stellige PIN für alle Actions mit 'confirm': true (0000-9999).</string>
<string id="SettingsPinError">Bitte eine gültige 4-stellige numerische PIN in den App Einstellungen eingeben (0000-9999).</string>
<string id="SettingsWidgetStart">(Nur Widget) Anwendung automatisch über das Widget starten ohne drauftippen zu müssen.</string>
<string id="SettingsEnableBatteryLevel">Hintergrunddienst aktivieren, um den Ladezustand der Batterie an HomeAssistant zu senden.</string>
<string id="SettingsBatteryLevelRefreshRate">Die Aktualisierungsrate (in Minuten) mit der der Ladezustand der Batterie

View File

@ -65,6 +65,12 @@
-->
<property id="confirm_timeout" type="number">3</property>
<!--
PIN to be used for confirmation. Used for actions with 'pin': true
which will show the PIN confirmation dialog.
-->
<property id="pin" type="string">0000</property>
<!--
Left to right or right to left text. Language dependent.
-->

View File

@ -79,6 +79,13 @@
<settingConfig type="numeric" min="0" />
</setting>
<setting
propertyKey="@Properties.pin"
title="@Strings.SettingsPin"
>
<settingConfig type="alphaNumeric" />
</setting>
<setting
propertyKey="@Properties.menu_alignment"
title="@Strings.SettingsTextAlign"

View File

@ -40,6 +40,9 @@
<string id="Empty">Empty</string>
<string id="TemplateError">Template Error</string>
<string id="PotentialError">Potential Error</string>
<string id="PinInputLocked">PIN input locked for</string>
<string id="Seconds">seconds</string>
<string id="WrongPin">Wrong PIN</string>
<!-- For the settings GUI -->
<string id="SettingsSelect">Select...</string>
@ -53,6 +56,8 @@
<string id="SettingsAppTimeout">Timeout in seconds. Exit the application after this period of inactivity to save the device battery.</string>
<string id="SettingsPollDelay">Additional poll delay (in seconds). Adds a delay between the status update of all menu items.</string>
<string id="SettingsConfirmTimeout">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.</string>
<string id="SettingsPin">4-digit PIN to be used for all actions that require confirmation (0000-9999).</string>
<string id="SettingsPinError">Please configure a valid 4-digit numeric PIN between 0000 and 9999 in the application settings.</string>
<string id="SettingsTextAlign">Left (off) or Right (on) Menu Alignment.</string>
<string id="LeftToRight">Left to right</string>
<string id="RightToLeft">Right to Left</string>

View File

@ -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
}

View File

@ -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 + ".");

View File

@ -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,

View File

@ -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<length; i++) {
mPinMask += "*";
}
requestUpdate();
}
}
class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
private var mPin as String;
private var mEnteredPin as 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) {
BehaviorDelegate.initialize();
mFailures = new PinFailures();
if (mFailures.isLocked()) {
var msg = WatchUi.loadResource($.Rez.Strings.PinInputLocked) + " " +
mFailures.getLockedUntilSeconds() + " " +
WatchUi.loadResource($.Rez.Strings.Seconds);
WatchUi.showToast(msg, {});
}
mPin = pin;
mEnteredPin = "";
mConfirmMethod = callback;
mState = state;
mView = view;
resetTimer();
}
function onSelectable(event as SelectableEvent) as Boolean {
if (mFailures.isLocked()) {
goBack();
}
var instance = event.getInstance();
if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) {
mEnteredPin += instance.getDigit();
createUserFeedback();
// System.println("HomeAssitantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
if (mEnteredPin.length() == mPin.length()) {
if (mEnteredPin.equals(mPin)) {
mFailures.reset();
getApp().getQuitTimer().reset();
if (mTimer != null) {
mTimer.stop();
}
mConfirmMethod.invoke(mState);
WatchUi.popView(WatchUi.SLIDE_RIGHT);
} else {
error();
}
} else {
resetTimer();
}
}
return true;
}
function createUserFeedback() {
if (Attention has :vibrate && Settings.getVibrate()) {
Attention.vibrate([new Attention.VibeProfile(25, 25)]);
}
mView.updatePinMask(mEnteredPin.length());
}
function resetTimer() {
var timeout = Settings.getConfirmTimeout(); // ms
if (timeout > 0) {
if (mTimer != null) {
mTimer.stop();
} else {
mTimer = new Timer.Timer();
}
mTimer.start(method(:goBack), timeout, false);
}
}
function goBack() as Void {
if (mTimer != null) {
mTimer.stop();
}
WatchUi.popView(WatchUi.SLIDE_RIGHT);
}
function error() as Void {
// System.println("HomeAssistantPinConfirmationDelegate error() Wrong PIN entered");
mFailures.addFailure();
if (Attention has :vibrate && Settings.getVibrate()) {
Attention.vibrate([
new Attention.VibeProfile(100, 100),
new Attention.VibeProfile(0, 200),
new Attention.VibeProfile(75, 100),
new Attention.VibeProfile(0, 200),
new Attention.VibeProfile(50, 100),
new Attention.VibeProfile(0, 200),
new Attention.VibeProfile(25, 100)
]);
}
if (WatchUi has :showToast) {
showToast($.Rez.Strings.WrongPin, null);
}
goBack();
}
}
class PinFailures {
const STORAGE_KEY_FAILURES as String = "pin_failures";
const STORAGE_KEY_LOCKED as String = "pin_locked";
private var mFailures as Array<Number>;
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;
}
}

View File

@ -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),

View File

@ -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);
}
}

View File

@ -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));
}

View File

@ -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
}