mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-08-03 10:28:41 +00:00
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.  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:
@ -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.
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
BIN
images/pin_view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
@ -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
|
||||
|
@ -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.
|
||||
-->
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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 + ".");
|
||||
|
@ -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,
|
||||
|
303
source/HomeAssistantPinConfirmation.mc
Normal file
303
source/HomeAssistantPinConfirmation.mc
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user