mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-08-04 19:08:36 +00:00
Finishing off widget+app code changes.
Glance now updates the status. Fix for quitting the application when persistently displaying an ErrorView. Added option for Widget RootView to immediately start HomeAssistant without waiting for a tap as requested by a user.
This commit is contained in:
@ -29,37 +29,37 @@ using Toybox.Timer;
|
||||
|
||||
class Alert extends WatchUi.View {
|
||||
private static const bRadius = 10;
|
||||
private var mTimer;
|
||||
private var mTimeout;
|
||||
private var mText;
|
||||
private var mFont;
|
||||
private var mFgcolor;
|
||||
private var mBgcolor;
|
||||
private var mTimer as Timer.Timer;
|
||||
private var mTimeout as Lang.Number;
|
||||
private var mText as Lang.String;
|
||||
private var mFont as Graphics.FontType;
|
||||
private var mFgcolor as Graphics.ColorType;
|
||||
private var mBgcolor as Graphics.ColorType;
|
||||
|
||||
function initialize(params as Lang.Dictionary) {
|
||||
View.initialize();
|
||||
|
||||
mText = params.get(:text);
|
||||
mText = params.get(:text) as Lang.String;
|
||||
if (mText == null) {
|
||||
mText = "Alert";
|
||||
}
|
||||
|
||||
mFont = params.get(:font);
|
||||
mFont = params.get(:font) as Graphics.FontType;
|
||||
if (mFont == null) {
|
||||
mFont = Graphics.FONT_MEDIUM;
|
||||
}
|
||||
|
||||
mFgcolor = params.get(:fgcolor);
|
||||
mFgcolor = params.get(:fgcolor) as Graphics.ColorType;
|
||||
if (mFgcolor == null) {
|
||||
mFgcolor = Graphics.COLOR_BLACK;
|
||||
}
|
||||
|
||||
mBgcolor = params.get(:bgcolor);
|
||||
mBgcolor = params.get(:bgcolor) as Graphics.ColorType;
|
||||
if (mBgcolor == null) {
|
||||
mBgcolor = Graphics.COLOR_WHITE;
|
||||
}
|
||||
|
||||
mTimeout = params.get(:timeout);
|
||||
mTimeout = params.get(:timeout) as Lang.Number;
|
||||
if (mTimeout == null) {
|
||||
mTimeout = 2000;
|
||||
}
|
||||
@ -83,7 +83,7 @@ class Alert extends WatchUi.View {
|
||||
var bX = (dc.getWidth() - bWidth) / 2;
|
||||
var bY = (dc.getHeight() - bHeight) / 2;
|
||||
|
||||
if(dc has :setAntiAlias) {
|
||||
if (dc has :setAntiAlias) {
|
||||
dc.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@ -112,30 +112,30 @@ class Alert extends WatchUi.View {
|
||||
|
||||
// Remove the alert from view, usually on user input, but that is defined by the calling function.
|
||||
//
|
||||
function dismiss() {
|
||||
function dismiss() as Void {
|
||||
WatchUi.popView(SLIDE_IMMEDIATE);
|
||||
}
|
||||
|
||||
function pushView(transition) {
|
||||
function pushView(transition) as Void {
|
||||
WatchUi.pushView(self, new AlertDelegate(self), transition);
|
||||
}
|
||||
}
|
||||
|
||||
class AlertDelegate extends WatchUi.InputDelegate {
|
||||
hidden var mView;
|
||||
private var mView;
|
||||
|
||||
function initialize(view) {
|
||||
InputDelegate.initialize();
|
||||
mView = view;
|
||||
}
|
||||
|
||||
function onKey(evt) {
|
||||
function onKey(evt) as Lang.Boolean {
|
||||
mView.dismiss();
|
||||
getApp().getQuitTimer().reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onTap(evt) {
|
||||
function onTap(evt) as Lang.Boolean {
|
||||
mView.dismiss();
|
||||
getApp().getQuitTimer().reset();
|
||||
return true;
|
||||
|
@ -33,6 +33,7 @@ using Toybox.Graphics;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Communications;
|
||||
using Toybox.Timer;
|
||||
|
||||
class ErrorView extends ScalableView {
|
||||
private var mText as Lang.String = "";
|
||||
@ -94,14 +95,15 @@ class ErrorView extends ScalableView {
|
||||
}
|
||||
if (!mShown) {
|
||||
instance.setText(text);
|
||||
mShown = true;
|
||||
}
|
||||
return [instance, instance.getDelegate()];
|
||||
}
|
||||
|
||||
// Create or reuse an existing ErrorView, and pass on the text.
|
||||
static function show(text as Lang.String) as Void {
|
||||
create(text); // Ignore returned values
|
||||
if (!mShown) {
|
||||
create(text); // Ignore returned values
|
||||
WatchUi.pushView(instance, instance.getDelegate(), WatchUi.SLIDE_UP);
|
||||
// This must be last to avoid a race condition with unShow(), where the
|
||||
// ErrorView can't be dismissed.
|
||||
@ -112,6 +114,10 @@ class ErrorView extends ScalableView {
|
||||
static function unShow() as Void {
|
||||
if (mShown) {
|
||||
WatchUi.popView(WatchUi.SLIDE_DOWN);
|
||||
// The call to 'updateNextMenuItem()' must be on another thread so that the view is popped above.
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiResume, false);
|
||||
// This must be last to avoid a race condition with show(), where the
|
||||
// ErrorView can't be dismissed.
|
||||
mShown = false;
|
||||
|
@ -26,5 +26,10 @@ class Globals {
|
||||
static const scDebug = false;
|
||||
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.
|
||||
static const scApiBackoff = 1000; // ms
|
||||
// Needs to be long enough to enable a "double ESC" to quit the application from
|
||||
// an ErrorView.
|
||||
static const scApiResume = 200; // ms
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using Toybox.Application;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Application.Properties;
|
||||
using Toybox.Timer;
|
||||
|
||||
class HomeAssistantApp extends Application.AppBase {
|
||||
private var strNoApiKey as Lang.String or Null;
|
||||
@ -37,19 +38,23 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
private var strTrailingSlashErr as Lang.String or Null;
|
||||
private var strAvailable = WatchUi.loadResource($.Rez.Strings.Available);
|
||||
private var strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable);
|
||||
private var strUnconfigured = WatchUi.loadResource($.Rez.Strings.Unconfigured);
|
||||
|
||||
private var mApiKey as Lang.String;
|
||||
private var mApiKey as Lang.String or Null; // The compiler can't tell these are updated by
|
||||
private var mApiUrl as Lang.String or Null; // initialize(), hence the "or Null".
|
||||
private var mConfigUrl as Lang.String or Null; //
|
||||
private var mApiStatus as Lang.String = WatchUi.loadResource($.Rez.Strings.Checking);
|
||||
private var mMenuStatus as Lang.String = WatchUi.loadResource($.Rez.Strings.Checking);
|
||||
private var mHaMenu as HomeAssistantView or Null;
|
||||
private var mQuitTimer as QuitTimer or Null;
|
||||
private var mQuitTimer as QuitTimer or Null;
|
||||
private var mTimer as Timer.Timer or Null;
|
||||
private var mItemsToUpdate; // Array initialised by onReturnFetchMenuConfig()
|
||||
private var mNextItemToUpdate = 0; // Index into the above array
|
||||
private var mIsGlance as Lang.Boolean = false;
|
||||
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
mApiKey = Properties.getValue("api_key");
|
||||
onSettingsChanged();
|
||||
// ATTENTION when adding stuff into this block:
|
||||
// Because of the >>GlanceView<<, it should contain only
|
||||
// code, which is used as well for the glance:
|
||||
@ -110,24 +115,22 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
strTrailingSlashErr = WatchUi.loadResource($.Rez.Strings.TrailingSlashErr);
|
||||
mQuitTimer = new QuitTimer();
|
||||
|
||||
var api_url = Properties.getValue("api_url") as Lang.String;
|
||||
|
||||
if ((Properties.getValue("api_key") as Lang.String).length() == 0) {
|
||||
if (mApiKey.length() == 0) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantApp getInitialView(): No API key in the application settings.");
|
||||
}
|
||||
return ErrorView.create(strNoApiKey + ".");
|
||||
} else if (api_url.length() == 0) {
|
||||
} else if (mApiUrl.length() == 0) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantApp getInitialView(): No API URL in the application settings.");
|
||||
}
|
||||
return ErrorView.create(strNoApiUrl + ".");
|
||||
} else if (api_url.substring(-1, api_url.length()).equals("/")) {
|
||||
} else if (mApiUrl.substring(-1, mApiUrl.length()).equals("/")) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantApp getInitialView(): API URL must not have a trailing slash '/'.");
|
||||
}
|
||||
return ErrorView.create(strTrailingSlashErr + ".");
|
||||
} else if ((Properties.getValue("config_url") as Lang.String).length() == 0) {
|
||||
} else if (mConfigUrl.length() == 0) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantApp getInitialView(): No configuration URL in the application settings.");
|
||||
}
|
||||
@ -215,6 +218,12 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
if (!mIsGlance) {
|
||||
mHaMenu = new HomeAssistantView(data, null);
|
||||
mQuitTimer.begin();
|
||||
if (Properties.getValue("widget_start_no_tap")) {
|
||||
// As soon as the menu has been fetched start show the menu of items.
|
||||
// This behaviour is inconsistent with the standard Garmin User Interface, but has been
|
||||
// requested by users so has been made the non-default option.
|
||||
pushHomeAssistantMenuView();
|
||||
}
|
||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||
// Start the continuous update process that continues for as long as the application is running.
|
||||
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
|
||||
@ -225,7 +234,6 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
WatchUi.switchToView(mHaMenu, new HomeAssistantViewDelegate(false), WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -237,20 +245,48 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
break;
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
(:glance)
|
||||
function fetchMenuConfig() as Void {
|
||||
var options = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
Communications.makeWebRequest(
|
||||
Properties.getValue("config_url"),
|
||||
null,
|
||||
options,
|
||||
method(:onReturnFetchMenuConfig)
|
||||
);
|
||||
if (mConfigUrl.equals("")) {
|
||||
mMenuStatus = strUnconfigured;
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
var options = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
}
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
ErrorView.show(strNoPhone + ".");
|
||||
}
|
||||
mMenuStatus = strUnavailable;
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||
}
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
ErrorView.show(strNoInternet + ".");
|
||||
}
|
||||
mMenuStatus = strUnavailable;
|
||||
} else {
|
||||
Communications.makeWebRequest(
|
||||
mConfigUrl,
|
||||
null,
|
||||
options,
|
||||
method(:onReturnFetchMenuConfig)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the API status.
|
||||
@ -322,7 +358,6 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
ErrorView.show("API " + mApiStatus + ".");
|
||||
}
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -333,23 +368,51 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
ErrorView.show(strUnhandledHttpErr + responseCode);
|
||||
}
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
(:glance)
|
||||
function fetchApiStatus() as Void {
|
||||
var options = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:headers => {
|
||||
"Authorization" => "Bearer " + mApiKey
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
Communications.makeWebRequest(
|
||||
Properties.getValue("api_url") + "/",
|
||||
null,
|
||||
options,
|
||||
method(:onReturnFetchApiStatus)
|
||||
);
|
||||
if (mApiUrl.equals("")) {
|
||||
mApiStatus = strUnconfigured;
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
var options = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:headers => {
|
||||
"Authorization" => "Bearer " + mApiKey
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
}
|
||||
mApiStatus = strUnavailable;
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
ErrorView.show(strNoPhone + ".");
|
||||
}
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||
}
|
||||
mApiStatus = strUnavailable;
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
ErrorView.show(strNoInternet + ".");
|
||||
}
|
||||
} else {
|
||||
Communications.makeWebRequest(
|
||||
mApiUrl + "/",
|
||||
null,
|
||||
options,
|
||||
method(:onReturnFetchApiStatus)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setApiStatus(s as Lang.String) {
|
||||
@ -387,11 +450,26 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
|
||||
(:glance)
|
||||
function getGlanceView() {
|
||||
function getGlanceView() as Lang.Array<WatchUi.GlanceView or WatchUi.GlanceViewDelegate> or Null {
|
||||
mIsGlance = true;
|
||||
updateGlance();
|
||||
mTimer = new Timer.Timer();
|
||||
mTimer.start(method(:updateGlance), Globals.scApiBackoff, true);
|
||||
return [new HomeAssistantGlanceView(self)];
|
||||
}
|
||||
|
||||
// Required for the Glance update timer.
|
||||
function updateGlance() as Void {
|
||||
fetchMenuConfig();
|
||||
fetchApiStatus();
|
||||
return [new HomeAssistantGlanceView(self)];
|
||||
}
|
||||
|
||||
// Replace this functionality with a more central settings class as proposed in
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant/pull/17.
|
||||
function onSettingsChanged() as Void {
|
||||
mApiKey = Properties.getValue("api_key");
|
||||
mApiUrl = Properties.getValue("api_url");
|
||||
mConfigUrl = Properties.getValue("config_url");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the status.
|
||||
// Terminate updating the toggle menu items via the chain of calls for a permanent network
|
||||
// error. The ErrorView cancellation will resume the call chain.
|
||||
//
|
||||
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
if (Globals.scDebug) {
|
||||
@ -74,8 +76,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data);
|
||||
}
|
||||
|
||||
// Provide the ability to terminate updating chain of calls for a permanent network error.
|
||||
var keepUpdating = true;
|
||||
var status = strUnavailable;
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
@ -111,8 +111,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
|
||||
}
|
||||
// Pause updates
|
||||
keepUpdating = false;
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
|
||||
@ -135,7 +133,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
ErrorView.show(strApiUrlNotFound);
|
||||
}
|
||||
keepUpdating = false;
|
||||
break;
|
||||
|
||||
case 405:
|
||||
@ -143,7 +140,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 405. " + mIdentifier + " " + data.get("message"));
|
||||
}
|
||||
ErrorView.show("HTTP 405, " + mIdentifier + ". " + data.get("message"));
|
||||
keepUpdating = false;
|
||||
|
||||
break;
|
||||
|
||||
case 200:
|
||||
@ -157,6 +154,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
setUiToggle(state);
|
||||
ErrorView.unShow();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
getApp().updateNextMenuItem();
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -165,10 +164,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
ErrorView.show(strUnhandledHttpErr + responseCode);
|
||||
}
|
||||
if (keepUpdating) {
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
getApp().updateNextMenuItem();
|
||||
}
|
||||
getApp().setApiStatus(status);
|
||||
}
|
||||
|
||||
@ -180,17 +175,18 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
var keepUpdating = true;
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
}
|
||||
ErrorView.show(strNoPhone + ".");
|
||||
getApp().setApiStatus(strUnavailable);
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||
}
|
||||
ErrorView.show(strNoInternet + ".");
|
||||
getApp().setApiStatus(strUnavailable);
|
||||
} else {
|
||||
var url = Properties.getValue("api_url") + "/states/" + mIdentifier;
|
||||
if (Globals.scDebug) {
|
||||
@ -202,19 +198,6 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
options,
|
||||
method(:onReturnGetState)
|
||||
);
|
||||
// The update is called by onReturnGetState() instead
|
||||
keepUpdating = false;
|
||||
}
|
||||
// On temporary failure, keep the updating going.
|
||||
if (keepUpdating) {
|
||||
// Need to avoid an infinite loop where the pushed ErrorView does not appear before getState() is called again
|
||||
// and the call stack overflows. So continue the call chain from somewhere asynchronous.
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), 500, false);
|
||||
if (Globals.scDebug) {
|
||||
System.println("HomeAssistantToggleMenuItem getState(): Updated failed " + mIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user