mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-09-13 04:31:32 +00:00
add Wifi LTE command execution (#255)
Firstly, thank you to all the maintainers for your work on this very useful application. This PR adds the ability to execute Home Assistant commands over Wi-Fi or LTE, behind an opt-in setting. While not a full "Wi-Fi mode," this feature allows the app to function in limited scenarios where the phone is unavailable but the device still has a direct network connection (e.g. Wi-Fi or LTE). When enabled: - The app can launch with a cached menu without phone connection. - On command execution, the user is prompted to confirm. - Upon confirmation, a bulk sync is triggered to send the request to Home Assistant. This enables basic control even without Bluetooth connectivity — so I can switch off that pesky bathroom light late at night when I don't have my phone nearby. I’ve seen a few issues suggesting similar functionality, and I believe this strikes a useful balance between functionality and simplicity. That said, I understand if this doesn't align with the intended featureset — feel free to close if so. This PR also adds a few string resources without translations. I'm not sure if those are autogenerated or user-provided — happy to adjust if needed.
This commit is contained in:
@@ -96,4 +96,9 @@
|
|||||||
-->
|
-->
|
||||||
<property id="webhook_id" type="string"></property>
|
<property id="webhook_id" type="string"></property>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enables the SyncDelegate and prompt to send a command over Wifi/LTE.
|
||||||
|
This will only show when not connected to the user's phone.
|
||||||
|
-->
|
||||||
|
<property id="wifi_lte_execution" type="boolean">false</property>
|
||||||
</properties>
|
</properties>
|
||||||
|
@@ -116,4 +116,13 @@
|
|||||||
>
|
>
|
||||||
<settingConfig type="alphaNumeric" readonly="true" />
|
<settingConfig type="alphaNumeric" readonly="true" />
|
||||||
</setting>
|
</setting>
|
||||||
|
|
||||||
|
<group enableIfTrue="@Properties.cache_config" id="wifiLteExection" title="@Strings.WifiLteExecution" description="@Strings.WifiLteExecutionDescription">
|
||||||
|
<setting
|
||||||
|
propertyKey="@Properties.wifi_lte_execution"
|
||||||
|
title="@Strings.WifiLteExecutionEnable"
|
||||||
|
>
|
||||||
|
<settingConfig type="boolean" />
|
||||||
|
</setting>
|
||||||
|
</group>
|
||||||
</settings>
|
</settings>
|
||||||
|
@@ -31,7 +31,9 @@
|
|||||||
<string id="NoInternet">No Internet connection.</string>
|
<string id="NoInternet">No Internet connection.</string>
|
||||||
<string id="NoJson">No JSON returned from HTTP request.</string>
|
<string id="NoJson">No JSON returned from HTTP request.</string>
|
||||||
<string id="NoPhone" scope="glance">No Phone connection.</string>
|
<string id="NoPhone" scope="glance">No Phone connection.</string>
|
||||||
|
<string id="NoPhoneNoCache" scope="glance">No phone connection, no cached menu.</string>
|
||||||
<string id="NoResponse">No Response, check Internet connection</string>
|
<string id="NoResponse">No Response, check Internet connection</string>
|
||||||
|
<string id="TimedOut">Request timed out</string>
|
||||||
<string id="PinInputLocked">PIN input locked for</string>
|
<string id="PinInputLocked">PIN input locked for</string>
|
||||||
<string id="PotentialError">Potential Error</string>
|
<string id="PotentialError">Potential Error</string>
|
||||||
<string id="Seconds">seconds</string>
|
<string id="Seconds">seconds</string>
|
||||||
@@ -42,6 +44,10 @@
|
|||||||
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
|
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
|
||||||
<string id="WebhookFailed">Failed to register Webhook</string>
|
<string id="WebhookFailed">Failed to register Webhook</string>
|
||||||
<string id="WrongPin">Wrong PIN</string>
|
<string id="WrongPin">Wrong PIN</string>
|
||||||
|
<string id="WifiLteNotAvailable">No Wifi or LTE available</string>
|
||||||
|
<string id="WifiLtePrompt">Execute over Wifi/LTE?</string>
|
||||||
|
<string id="WifiLteExecutionTitle">Sending to Home Assistant.</string>
|
||||||
|
<string id="WifiLteExecutionDataError">No data received.</string>
|
||||||
|
|
||||||
<!-- For the settings GUI, strings should be in the order they are used. -->
|
<!-- For the settings GUI, strings should be in the order they are used. -->
|
||||||
<string id="SettingsSelect">Select...</string>
|
<string id="SettingsSelect">Select...</string>
|
||||||
@@ -64,4 +70,7 @@
|
|||||||
<string id="SettingsEnableBatteryLevel">Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant.</string>
|
<string id="SettingsEnableBatteryLevel">Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant.</string>
|
||||||
<string id="SettingsBatteryLevelRefreshRate">The refresh rate (in minutes) at which the background service should repeat sending data.</string>
|
<string id="SettingsBatteryLevelRefreshRate">The refresh rate (in minutes) at which the background service should repeat sending data.</string>
|
||||||
<string id="WebhookId">(Read only) The Webhook ID created by the device for background service updates. You might require this for debugging.</string>
|
<string id="WebhookId">(Read only) The Webhook ID created by the device for background service updates. You might require this for debugging.</string>
|
||||||
|
<string id="WifiLteExecution">Wifi/LTE execution mode.</string>
|
||||||
|
<string id="WifiLteExecutionEnable">Enable executing commands over Wifi/LTE.</string>
|
||||||
|
<string id="WifiLteExecutionDescription">Allows the app to start without phone connection (when menu is cached), and prompt to execute command over Wifi/LTE.</string>
|
||||||
</strings>
|
</strings>
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
|
|
||||||
using Toybox.Application;
|
using Toybox.Application;
|
||||||
|
using Toybox.Communications;
|
||||||
using Toybox.Lang;
|
using Toybox.Lang;
|
||||||
using Toybox.WatchUi;
|
using Toybox.WatchUi;
|
||||||
using Toybox.System;
|
using Toybox.System;
|
||||||
@@ -25,6 +26,7 @@ using Toybox.Timer;
|
|||||||
(:glance, :background)
|
(:glance, :background)
|
||||||
class HomeAssistantApp extends Application.AppBase {
|
class HomeAssistantApp extends Application.AppBase {
|
||||||
private var mApiStatus as Lang.String or Null;
|
private var mApiStatus as Lang.String or Null;
|
||||||
|
private var mHasToast as Lang.Boolean = false;
|
||||||
private var mMenuStatus as Lang.String or Null;
|
private var mMenuStatus as Lang.String or Null;
|
||||||
private var mHaMenu as HomeAssistantView or Null;
|
private var mHaMenu as HomeAssistantView or Null;
|
||||||
private var mGlanceTemplate as Lang.String or Null = null;
|
private var mGlanceTemplate as Lang.String or Null = null;
|
||||||
@@ -38,6 +40,9 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
private var mIsApp as Lang.Boolean = false; // Or Widget
|
||||||
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||||
private var mTemplates as Lang.Dictionary = {};
|
private var mTemplates as Lang.Dictionary = {};
|
||||||
|
private var mNotifiedNoBle as Lang.Boolean = false;
|
||||||
|
|
||||||
|
private const wifiPollDelayMs = 2000;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//
|
//
|
||||||
@@ -105,6 +110,7 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
mUpdateTimer = new Timer.Timer();
|
mUpdateTimer = new Timer.Timer();
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||||
|
mHasToast = WatchUi has :showToast;
|
||||||
Settings.update();
|
Settings.update();
|
||||||
|
|
||||||
if (Settings.getApiKey().length() == 0) {
|
if (Settings.getApiKey().length() == 0) {
|
||||||
@@ -122,11 +128,14 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
} else if (Settings.getPin() == null) {
|
} else if (Settings.getPin() == null) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
|
// System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().phoneConnected) {
|
} else if (! System.getDeviceSettings().phoneConnected and Settings.getWifiLteExecutionEnabled() and ! hasCachedMenu()) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp getInitialView(): No Phone connection, no cached menu, skipping API call.");
|
||||||
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhoneNoCache) as Lang.String);
|
||||||
|
} else if (! System.getDeviceSettings().phoneConnected and ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
// System.println("HomeAssistantApp getInitialView(): No Phone connection and wifi disabled, skipping API call.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! System.getDeviceSettings().connectionAvailable and ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp getInitialView(): No Internet connection and wifi disabled, skipping API call.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
var isCached = fetchMenuConfig();
|
var isCached = fetchMenuConfig();
|
||||||
@@ -227,6 +236,20 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Can we use the cached menu?
|
||||||
|
//!
|
||||||
|
//! @return Return true if there's a menu in cache, and if the user has enabled the cache and
|
||||||
|
//! has not requested to have the cache busted.
|
||||||
|
//
|
||||||
|
function hasCachedMenu() as Lang.Boolean {
|
||||||
|
if (Settings.getClearCache() || !Settings.getCacheConfig()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = Storage.getValue("menu") as Lang.Dictionary;
|
||||||
|
return menu != null;
|
||||||
|
}
|
||||||
|
|
||||||
//! Fetch the menu configuration over HTTPS, which might be locally cached.
|
//! Fetch the menu configuration over HTTPS, which might be locally cached.
|
||||||
//!
|
//!
|
||||||
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
|
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
|
||||||
@@ -246,22 +269,22 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
Settings.unsetClearCache();
|
Settings.unsetClearCache();
|
||||||
}
|
}
|
||||||
if (menu == null) {
|
if (menu == null) {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if (! phoneConnected or ! internetAvailable) {
|
||||||
|
var errorRez = $.Rez.Strings.NoPhone;
|
||||||
|
if (Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
errorRez = $.Rez.Strings.NoPhoneNoCache;
|
||||||
|
} else if (! internetAvailable) {
|
||||||
|
errorRez = $.Rez.Strings.Unavailable;
|
||||||
|
}
|
||||||
// System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call.");
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
|
||||||
}
|
}
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
|
||||||
// System.println("HomeAssistantApp fetchMenuConfig(): No Internet connection, skipping API call.");
|
|
||||||
if (mIsGlance) {
|
|
||||||
WatchUi.requestUpdate();
|
|
||||||
} else {
|
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
|
||||||
}
|
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
|
||||||
} else {
|
} else {
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
Settings.getConfigUrl(),
|
Settings.getConfigUrl(),
|
||||||
@@ -301,11 +324,11 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
|
|
||||||
//! Start the periodic menu updates for as long as the application is running.
|
//! Start the periodic menu updates for as long as the application is running.
|
||||||
//
|
//
|
||||||
function startUpdates() {
|
function startUpdates() as Void {
|
||||||
if (mHaMenu != null and !mUpdating) {
|
if (mHaMenu != null and !mUpdating) {
|
||||||
// Start the continuous update process that continues for as long as the application is running.
|
// Start the continuous update process that continues for as long as the application is running.
|
||||||
updateMenuItems();
|
|
||||||
mUpdating = true;
|
mUpdating = true;
|
||||||
|
updateMenuItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,15 +433,50 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
//! Construct the GET request to update all menu items.
|
//! Construct the GET request to update all menu items.
|
||||||
//
|
//
|
||||||
function updateMenuItems() as Void {
|
function updateMenuItems() as Void {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
|
||||||
|
// In Wifi/LTE execution mode, we should not show an error page but use a toast instead.
|
||||||
|
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
|
||||||
|
// Notify only once per disconnection cycle
|
||||||
|
if (!mNotifiedNoBle) {
|
||||||
|
var toast = WatchUi.loadResource($.Rez.Strings.NoPhone);
|
||||||
|
if (!connectionAvailable) {
|
||||||
|
toast = WatchUi.loadResource($.Rez.Strings.NoInternet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHasToast) {
|
||||||
|
WatchUi.showToast(toast, null);
|
||||||
|
} else {
|
||||||
|
new Alert({
|
||||||
|
:timeout => Globals.scAlertTimeout,
|
||||||
|
:font => Graphics.FONT_MEDIUM,
|
||||||
|
:text => toast,
|
||||||
|
:fgcolor => Graphics.COLOR_WHITE,
|
||||||
|
:bgcolor => Graphics.COLOR_BLACK
|
||||||
|
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mNotifiedNoBle = true;
|
||||||
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
|
mUpdateTimer.start(method(:startUpdates), wifiPollDelayMs, false);
|
||||||
|
|
||||||
|
mUpdating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! connectionAvailable) {
|
||||||
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
|
mNotifiedNoBle = false;
|
||||||
|
|
||||||
if (mItemsToUpdate == null or mTemplates == null) {
|
if (mItemsToUpdate == null or mTemplates == null) {
|
||||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||||
mTemplates = {};
|
mTemplates = {};
|
||||||
@@ -531,20 +589,27 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
//
|
//
|
||||||
(:glance)
|
(:glance)
|
||||||
function fetchApiStatus() as Void {
|
function fetchApiStatus() as Void {
|
||||||
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
|
||||||
// System.println("API URL = " + Settings.getApiUrl());
|
// System.println("API URL = " + Settings.getApiUrl());
|
||||||
if (Settings.getApiUrl().equals("")) {
|
if (Settings.getApiUrl().equals("")) {
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
if (! mIsGlance && Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
|
||||||
|
// System.println("HomeAssistantApp fetchApiStatus(): In-app Wifi mode (No Phone and Internet connection), early return.");
|
||||||
|
return;
|
||||||
|
} else if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantApp fetchApiStatus(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchApiStatus(): No Phone connection, skipping API call.");
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
|
System.println("we here");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
}
|
}
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! connectionAvailable) {
|
||||||
// System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call.");
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
@@ -785,6 +850,13 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
return mIsApp;
|
return mIsApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Returns a SyncDelegate for this App
|
||||||
|
//!
|
||||||
|
//! @return a SyncDelegate or null
|
||||||
|
//
|
||||||
|
public function getSyncDelegate() as Communications.SyncDelegate? {
|
||||||
|
return new HomeAssistantSyncDelegate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Global function to return the application object.
|
//! Global function to return the application object.
|
||||||
|
@@ -35,19 +35,43 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation {
|
|||||||
//! Delegate to respond to the confirmation request.
|
//! Delegate to respond to the confirmation request.
|
||||||
//
|
//
|
||||||
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||||
|
private static var mTimer as Timer.Timer or Null;
|
||||||
|
|
||||||
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
|
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 mState as Lang.Boolean;
|
||||||
|
private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null;
|
||||||
|
private var mConfirmationView as WatchUi.Confirmation;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
|
//!
|
||||||
|
//! @param options A dictionary describing the following options:
|
||||||
|
//! - callback Method to call on confirmation.
|
||||||
|
//! - confirmationView Confirmation the delegate is active for
|
||||||
|
//! - state Wanted state of a toggle button.
|
||||||
|
//! - toggle Optional setEnabled method to untoggle ToggleItem.
|
||||||
//
|
//
|
||||||
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) {
|
function initialize(options as {
|
||||||
|
:callback as Method(state as Lang.Boolean) as Void,
|
||||||
|
:confirmationView as WatchUi.Confirmation,
|
||||||
|
:state as Lang.Boolean,
|
||||||
|
:toggleMethod as Method(state as Lang.Boolean) or Null,
|
||||||
|
}) {
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
WatchUi.ConfirmationDelegate.initialize();
|
WatchUi.ConfirmationDelegate.initialize();
|
||||||
mConfirmMethod = callback;
|
mConfirmMethod = options[:callback];
|
||||||
mState = state;
|
mConfirmationView = options[:confirmationView];
|
||||||
|
mState = options[:state];
|
||||||
|
mToggleMethod = options[:toggleMethod];
|
||||||
|
|
||||||
var timeout = Settings.getConfirmTimeout(); // ms
|
var timeout = Settings.getConfirmTimeout(); // ms
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
|
if (mTimer == null) {
|
||||||
mTimer = new Timer.Timer();
|
mTimer = new Timer.Timer();
|
||||||
|
}
|
||||||
|
|
||||||
mTimer.start(method(:onTimeout), timeout, true);
|
mTimer.start(method(:onTimeout), timeout, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +88,11 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
|||||||
}
|
}
|
||||||
if (response == WatchUi.CONFIRM_YES) {
|
if (response == WatchUi.CONFIRM_YES) {
|
||||||
mConfirmMethod.invoke(mState);
|
mConfirmMethod.invoke(mState);
|
||||||
|
} else {
|
||||||
|
// Undo the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -71,6 +100,14 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
|||||||
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
||||||
function onTimeout() as Void {
|
function onTimeout() as Void {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
|
// Undo the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var getCurrentView = WatchUi.getCurrentView();
|
||||||
|
if (getCurrentView[0] == mConfirmationView) {
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -185,21 +185,25 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
private var mTimer as Timer.Timer or Null;
|
private var mTimer as Timer.Timer or Null;
|
||||||
private var mState as Lang.Boolean;
|
private var mState as Lang.Boolean;
|
||||||
private var mFailures as PinFailures;
|
private var mFailures as PinFailures;
|
||||||
|
private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null;
|
||||||
private var mView as HomeAssistantPinConfirmationView;
|
private var mView as HomeAssistantPinConfirmationView;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//!
|
//!
|
||||||
//! @param callback Method to call on confirmation.
|
//! @param options A dictionary describing the following options:
|
||||||
//! @param state Current state of a toggle button.
|
//! - callback Method to call on confirmation.
|
||||||
//! @param pin PIN to be matched.
|
//! - pin PIN to be matched.
|
||||||
//! @param view PIN confirmation view.
|
//! - state Wanted state of a toggle button.
|
||||||
|
//! - toggle Optional setEnabled method to untoggle ToggleItem.
|
||||||
|
//! - view PIN confirmation view.
|
||||||
//
|
//
|
||||||
function initialize(
|
function initialize(options as {
|
||||||
callback as Method(state as Lang.Boolean) as Void,
|
:callback as Method(state as Lang.Boolean) as Void,
|
||||||
state as Lang.Boolean,
|
:pin as Lang.String,
|
||||||
pin as Lang.String,
|
:state as Lang.Boolean,
|
||||||
view as HomeAssistantPinConfirmationView
|
:view as HomeAssistantPinConfirmationView,
|
||||||
) {
|
:toggleMethod as (Method(state as Lang.Boolean) as Void) or Null,
|
||||||
|
}) {
|
||||||
BehaviorDelegate.initialize();
|
BehaviorDelegate.initialize();
|
||||||
mFailures = new PinFailures();
|
mFailures = new PinFailures();
|
||||||
if (mFailures.isLocked()) {
|
if (mFailures.isLocked()) {
|
||||||
@@ -208,11 +212,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
WatchUi.loadResource($.Rez.Strings.Seconds);
|
WatchUi.loadResource($.Rez.Strings.Seconds);
|
||||||
WatchUi.showToast(msg, {});
|
WatchUi.showToast(msg, {});
|
||||||
}
|
}
|
||||||
mPin = pin;
|
mPin = options[:pin];
|
||||||
mEnteredPin = "";
|
mEnteredPin = "";
|
||||||
mConfirmMethod = callback;
|
mConfirmMethod = options[:callback];
|
||||||
mState = state;
|
mState = options[:state];
|
||||||
mView = view;
|
mToggleMethod = options[:toggleMethod];
|
||||||
|
mView = options[:view];
|
||||||
|
|
||||||
resetTimer();
|
resetTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +243,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
if (mTimer != null) {
|
if (mTimer != null) {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
}
|
}
|
||||||
mConfirmMethod.invoke(mState);
|
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
|
|
||||||
|
// Set the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
|
mConfirmMethod.invoke(mState);
|
||||||
} else {
|
} else {
|
||||||
error();
|
error();
|
||||||
}
|
}
|
||||||
@@ -279,6 +290,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
if (mTimer != null) {
|
if (mTimer != null) {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +316,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
goBack();
|
goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Handle the back button (ESC)
|
||||||
|
//
|
||||||
|
function onBack() as Lang.Boolean {
|
||||||
|
goBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,9 +27,8 @@ class HomeAssistantService {
|
|||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//
|
//
|
||||||
function initialize() {
|
function initialize() {
|
||||||
if (WatchUi has :showToast) {
|
mHasToast = WatchUi has :showToast;
|
||||||
mHasToast = true;
|
|
||||||
}
|
|
||||||
if (Attention has :vibrate) {
|
if (Attention has :vibrate) {
|
||||||
mHasVibrate = true;
|
mHasVibrate = true;
|
||||||
}
|
}
|
||||||
@@ -129,10 +128,25 @@ class HomeAssistantService {
|
|||||||
data as Lang.Dictionary or Null,
|
data as Lang.Dictionary or Null,
|
||||||
exit as Lang.Boolean
|
exit as Lang.Boolean
|
||||||
) as Void {
|
) as Void {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! internetAvailable)) {
|
||||||
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
|
WatchUi.pushView(
|
||||||
|
dialog,
|
||||||
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "service",
|
||||||
|
:service => service,
|
||||||
|
:data => data,
|
||||||
|
:exit => exit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
} else if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! internetAvailable) {
|
||||||
// System.println("HomeAssistantService call(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantService call(): No Internet connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
|
111
source/HomeAssistantSyncDelegate.mc
Normal file
111
source/HomeAssistantSyncDelegate.mc
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using Toybox.Communications;
|
||||||
|
using Toybox.Lang;
|
||||||
|
|
||||||
|
// SyncDelegate to execute single command via POST request to Home Assistant
|
||||||
|
//
|
||||||
|
class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
|
||||||
|
private static var syncError as Lang.String or Null;
|
||||||
|
|
||||||
|
// Initialize an instance of this delegate
|
||||||
|
public function initialize() {
|
||||||
|
SyncDelegate.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Called by the system to determine if a sync is needed
|
||||||
|
public function isSyncNeeded() as Lang.Boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Called by the system when starting a bulk sync.
|
||||||
|
public function onStartSync() as Void {
|
||||||
|
syncError = null;
|
||||||
|
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData == null) {
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionDataError) as Lang.String;
|
||||||
|
onStopSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = WifiLteExecutionConfirmDelegate.mCommandData[:type];
|
||||||
|
var data = WifiLteExecutionConfirmDelegate.mCommandData[:data];
|
||||||
|
var url;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "service":
|
||||||
|
var service = WifiLteExecutionConfirmDelegate.mCommandData[:service];
|
||||||
|
url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length());
|
||||||
|
var entity_id = "";
|
||||||
|
if (data != null) {
|
||||||
|
entity_id = data.get("entity_id");
|
||||||
|
if (entity_id == null) {
|
||||||
|
entity_id = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performRequest(url, data);
|
||||||
|
break;
|
||||||
|
case "entity":
|
||||||
|
url = WifiLteExecutionConfirmDelegate.mCommandData[:url];
|
||||||
|
performRequest(url, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a POST request to Hass with a given payload and URL, and calls haCallback
|
||||||
|
private function performRequest(url as Lang.String, data as Lang.Dictionary or Null) {
|
||||||
|
Communications.makeWebRequest(
|
||||||
|
url,
|
||||||
|
data, // May include {"entity_id": xxxx} for service calls
|
||||||
|
{
|
||||||
|
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||||
|
:headers => {
|
||||||
|
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
|
||||||
|
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||||
|
},
|
||||||
|
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
|
||||||
|
},
|
||||||
|
method(:haCallback)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Handle callback from request
|
||||||
|
public function haCallback(code as Lang.Number, data as Null or Lang.Dictionary) as Void {
|
||||||
|
Communications.notifySyncProgress(100);
|
||||||
|
if (code == 200) {
|
||||||
|
syncError = null;
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) {
|
||||||
|
var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback];
|
||||||
|
if (callbackMethod != null) {
|
||||||
|
var d = data as Lang.Array;
|
||||||
|
callbackMethod.invoke(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onStopSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(code) {
|
||||||
|
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.TimedOut) as Lang.String;
|
||||||
|
break;
|
||||||
|
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String;
|
||||||
|
syncError = "";
|
||||||
|
default:
|
||||||
|
var codeMsg = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String;
|
||||||
|
syncError = codeMsg + code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onStopSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Clean up
|
||||||
|
public function onStopSync() as Void {
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData[:exit]) {
|
||||||
|
System.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Communications.cancelAllRequests();
|
||||||
|
Communications.notifySyncComplete(syncError);
|
||||||
|
}
|
||||||
|
}
|
@@ -82,16 +82,43 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
|||||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
pinConfirmationView,
|
pinConfirmationView,
|
||||||
new HomeAssistantPinConfirmationDelegate(method(:onConfirm), false, pin, pinConfirmationView),
|
new HomeAssistantPinConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:pin => pin,
|
||||||
|
:state => false,
|
||||||
|
:view => pinConfirmationView,
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (mConfirm) {
|
} else if (mConfirm) {
|
||||||
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
new HomeAssistantConfirmation(),
|
dialog,
|
||||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "service",
|
||||||
|
:service => mService,
|
||||||
|
:data => mData,
|
||||||
|
:exit => mExit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var view = new HomeAssistantConfirmation();
|
||||||
|
WatchUi.pushView(
|
||||||
|
view,
|
||||||
|
new HomeAssistantConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:confirmationView => view,
|
||||||
|
:state => false,
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onConfirm(false);
|
onConfirm(false);
|
||||||
}
|
}
|
||||||
|
@@ -198,16 +198,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
case 200:
|
case 200:
|
||||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
||||||
getApp().forceStatusUpdates();
|
getApp().forceStatusUpdates();
|
||||||
var state;
|
|
||||||
var d = data as Lang.Array;
|
var d = data as Lang.Array;
|
||||||
for(var i = 0; i < d.size(); i++) {
|
setToggleStateWithData(d);
|
||||||
if ((d[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) {
|
|
||||||
state = d[i].get("state") as Lang.String;
|
|
||||||
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
|
|
||||||
setUiToggle(state);
|
|
||||||
WatchUi.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -221,30 +213,54 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Handles the response from a Home Assistant service or state call and updates the toggle UI.
|
||||||
|
//!
|
||||||
|
//! @param data An array of dictionaries, each representing a Home Assistant entity state.
|
||||||
|
//
|
||||||
|
function setToggleStateWithData(data as Lang.Array) {
|
||||||
|
// if there's no response body, let's assume that what we did, happened, and flip the toggle
|
||||||
|
if (data.size() == 0) {
|
||||||
|
setEnabled(!isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
for(var i = 0; i < data.size(); i++) {
|
||||||
|
if ((data[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) {
|
||||||
|
var state = data[i].get("state") as Lang.String;
|
||||||
|
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
|
||||||
|
setUiToggle(state);
|
||||||
|
WatchUi.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//! Set the state of the toggle menu item.
|
//! Set the state of the toggle menu item.
|
||||||
//!
|
//!
|
||||||
//! @param s Boolean indicating the desired state of the toggle switch.
|
//! @param s Boolean indicating the desired state of the toggle switch.
|
||||||
//
|
//
|
||||||
function setState(s as Lang.Boolean) as Void {
|
function setState(s as Lang.Boolean) as Void {
|
||||||
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
setEnabled(!isEnabled());
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
|
||||||
|
if (! phoneConnected && ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! internetAvailable && ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||||
// Toggle the UI back
|
// Toggle the UI back
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
// Updated SDK and got a new error
|
|
||||||
// ERROR: venu: Cannot find symbol ':substring' on type 'PolyType<Null or $.Toybox.Lang.Object>'.
|
|
||||||
var id = mData.get("entity_id") as Lang.String;
|
var id = mData.get("entity_id") as Lang.String;
|
||||||
var url = Settings.getApiUrl() + "/services/";
|
var url = getUrl(id, s);
|
||||||
if (s) {
|
|
||||||
url = url + id.substring(0, id.find(".")) + "/turn_on";
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
} else {
|
// Undo the toggle
|
||||||
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
setEnabled(!isEnabled());
|
||||||
|
wifiPrompt(s);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
||||||
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
@@ -275,21 +291,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
function callService(b as Lang.Boolean) as Void {
|
function callService(b as Lang.Boolean) as Void {
|
||||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||||
if (mPin && hasTouchScreen) {
|
if (mPin && hasTouchScreen) {
|
||||||
|
// Undo the toggle
|
||||||
|
setEnabled(!isEnabled());
|
||||||
|
|
||||||
var pin = Settings.getPin();
|
var pin = Settings.getPin();
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
pinConfirmationView,
|
pinConfirmationView,
|
||||||
new HomeAssistantPinConfirmationDelegate(method(:onConfirm), b, pin, pinConfirmationView),
|
new HomeAssistantPinConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:pin => pin,
|
||||||
|
:state => b,
|
||||||
|
:toggleMethod => method(:setEnabled),
|
||||||
|
:view => pinConfirmationView,
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (mConfirm) {
|
} else if (mConfirm) {
|
||||||
|
// Undo the toggle
|
||||||
|
setEnabled(!isEnabled());
|
||||||
|
|
||||||
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
wifiPrompt(b);
|
||||||
|
} else {
|
||||||
|
var confirmationView = new HomeAssistantConfirmation();
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
new HomeAssistantConfirmation(),
|
confirmationView,
|
||||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), b),
|
new HomeAssistantConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:confirmationView => confirmationView,
|
||||||
|
:state => b,
|
||||||
|
:toggleMethod => method(:setEnabled),
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onConfirm(b);
|
onConfirm(b);
|
||||||
}
|
}
|
||||||
@@ -303,4 +343,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
setState(b);
|
setState(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Displays a confirmation dialog before executing a service call via Wi-Fi/LTE.
|
||||||
|
//!
|
||||||
|
//! @param s Desired state: `true` to turn on, `false` to turn off.
|
||||||
|
//
|
||||||
|
private function wifiPrompt(s as Lang.Boolean) as Void {
|
||||||
|
var id = mData.get("entity_id") as Lang.String;
|
||||||
|
var url = getUrl(id, s);
|
||||||
|
|
||||||
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
|
WatchUi.pushView(
|
||||||
|
dialog,
|
||||||
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "entity",
|
||||||
|
:url => url,
|
||||||
|
:id => id,
|
||||||
|
:data => mData,
|
||||||
|
:callback => method(:setToggleStateWithData),
|
||||||
|
:exit => mExit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Constructs a Home Assistant API URL for the given entity and desired state.
|
||||||
|
//!
|
||||||
|
//! @param id The entity ID, e.g., `"switch.kitchen"`.
|
||||||
|
//! @param s Desired state: `true` for "turn_on", `false` for "turn_off".
|
||||||
|
//!
|
||||||
|
//! @return Full service URL string.
|
||||||
|
//
|
||||||
|
private function getUrl(id as Lang.String, s as Lang.Boolean) as Lang.String {
|
||||||
|
var url = Settings.getApiUrl() + "/services/";
|
||||||
|
if (s) {
|
||||||
|
url = url + id.substring(0, id.find(".")) + "/turn_on";
|
||||||
|
} else {
|
||||||
|
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,7 @@ class Settings {
|
|||||||
private static var mCacheConfig as Lang.Boolean = false;
|
private static var mCacheConfig as Lang.Boolean = false;
|
||||||
private static var mClearCache as Lang.Boolean = false;
|
private static var mClearCache as Lang.Boolean = false;
|
||||||
private static var mVibrate as Lang.Boolean = false;
|
private static var mVibrate as Lang.Boolean = false;
|
||||||
|
private static var mWifiLteExecution as Lang.Boolean = false;
|
||||||
//! seconds
|
//! seconds
|
||||||
private static var mAppTimeout as Lang.Number = 0;
|
private static var mAppTimeout as Lang.Number = 0;
|
||||||
//! seconds
|
//! seconds
|
||||||
@@ -69,6 +70,7 @@ class Settings {
|
|||||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||||
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
||||||
|
mWifiLteExecution = Properties.getValue("wifi_lte_execution");
|
||||||
}
|
}
|
||||||
|
|
||||||
//! A webhook is required for non-privileged API calls.
|
//! A webhook is required for non-privileged API calls.
|
||||||
@@ -270,4 +272,16 @@ class Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Get the value of the WiFi/LTE toggle in settings.
|
||||||
|
//!
|
||||||
|
//! @return The state of the toggle.
|
||||||
|
//
|
||||||
|
static function getWifiLteExecutionEnabled() as Lang.Boolean {
|
||||||
|
// Wifi/LTE sync execution on a cached menu
|
||||||
|
if (!mCacheConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mWifiLteExecution;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
135
source/WifiLteExecutionConfirmDelegate.mc
Normal file
135
source/WifiLteExecutionConfirmDelegate.mc
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Toybox.WatchUi;
|
||||||
|
using Toybox.System;
|
||||||
|
using Toybox.Communications;
|
||||||
|
using Toybox.Lang;
|
||||||
|
using Toybox.Timer;
|
||||||
|
|
||||||
|
// Delegate to respond to a confirmation to execute command via bulk sync
|
||||||
|
//
|
||||||
|
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
|
||||||
|
public static var mCommandData as {
|
||||||
|
:type as Lang.String,
|
||||||
|
:service as Lang.String or Null,
|
||||||
|
:data as Lang.Dictionary or Null,
|
||||||
|
:url as Lang.String or Null,
|
||||||
|
:id as Lang.Number or Null,
|
||||||
|
:exit as Lang.Boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
private static var mTimer as Timer.Timer or Null;
|
||||||
|
private var mHasToast as Lang.Boolean = false;
|
||||||
|
private var mConfirmationView as WatchUi.Confirmation;
|
||||||
|
|
||||||
|
//! Initializes a confirmation delegate to confirm a Wi-Fi or LTE command exection
|
||||||
|
//!
|
||||||
|
//! @param options A dictionary describing the command to be executed:
|
||||||
|
//! - type: The command type, either `"service"` or `"entity"`.
|
||||||
|
//! - service: (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on").
|
||||||
|
//! - url: (For type `"entity"`) The full Home Assistant entity API URL.
|
||||||
|
//! - callback: (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.
|
||||||
|
//! - data: (Optional) A dictionary of data to send with the request.
|
||||||
|
//! - exit: Boolean: if set to true: exit after running command.
|
||||||
|
//! @param view The Confirmation view the delegate is active for
|
||||||
|
function initialize(cOptions as {
|
||||||
|
:type as Lang.String,
|
||||||
|
:service as Lang.String or Null,
|
||||||
|
:data as Lang.Dictionary or Null,
|
||||||
|
:url as Lang.String or Null,
|
||||||
|
:callback as Lang.Method or Null,
|
||||||
|
:exit as Lang.Boolean,
|
||||||
|
}, view as WatchUi.Confirmation) {
|
||||||
|
ConfirmationDelegate.initialize();
|
||||||
|
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WatchUi has :showToast) {
|
||||||
|
mHasToast = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mConfirmationView = view;
|
||||||
|
mCommandData = {
|
||||||
|
:type => cOptions[:type],
|
||||||
|
:service => cOptions[:service],
|
||||||
|
:data => cOptions[:data],
|
||||||
|
:url => cOptions[:url],
|
||||||
|
:callback => cOptions[:callback],
|
||||||
|
:exit => cOptions[:exit]
|
||||||
|
};
|
||||||
|
|
||||||
|
var timeout = Settings.getConfirmTimeout(); // ms
|
||||||
|
if (timeout > 0) {
|
||||||
|
if (mTimer == null) {
|
||||||
|
mTimer = new Timer.Timer();
|
||||||
|
}
|
||||||
|
|
||||||
|
mTimer.start(method(:onTimeout), timeout, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Handles the user's response to the confirmation dialog.
|
||||||
|
//!
|
||||||
|
//! @param response The user's confirmation response as `WatchUi.Confirm`
|
||||||
|
//! @return Always returns `true` to indicate the response was handled.
|
||||||
|
function onResponse(response) as Lang.Boolean {
|
||||||
|
getApp().getQuitTimer().reset();
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response == WatchUi.CONFIRM_YES) {
|
||||||
|
trySync();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Initiates a bulk sync process to execute a command, if connections are available
|
||||||
|
private function trySync() as Void {
|
||||||
|
var connectionInfo = System.getDeviceSettings().connectionInfo;
|
||||||
|
var keys = connectionInfo.keys();
|
||||||
|
var possibleConnection = false;
|
||||||
|
|
||||||
|
for(var i = 0; i < keys.size(); i++) {
|
||||||
|
if (keys[i] != :bluetooth) {
|
||||||
|
var connection = connectionInfo[keys[i]];
|
||||||
|
if (connection.state != System.CONNECTION_STATE_NOT_INITIALIZED) {
|
||||||
|
possibleConnection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibleConnection) {
|
||||||
|
if (Communications has :startSync2) {
|
||||||
|
var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String;
|
||||||
|
Communications.startSync2({:message => syncString});
|
||||||
|
} else {
|
||||||
|
Communications.startSync();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var toast = WatchUi.loadResource($.Rez.Strings.WifiLteNotAvailable) as Lang.String;
|
||||||
|
if (mHasToast) {
|
||||||
|
WatchUi.showToast(toast, null);
|
||||||
|
} else {
|
||||||
|
new Alert({
|
||||||
|
:timeout => Globals.scAlertTimeout,
|
||||||
|
:font => Graphics.FONT_MEDIUM,
|
||||||
|
:text => toast,
|
||||||
|
:fgcolor => Graphics.COLOR_WHITE,
|
||||||
|
:bgcolor => Graphics.COLOR_BLACK
|
||||||
|
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
||||||
|
function onTimeout() as Void {
|
||||||
|
mTimer.stop();
|
||||||
|
var getCurrentView = WatchUi.getCurrentView();
|
||||||
|
|
||||||
|
if (getCurrentView[0] == mConfirmationView) {
|
||||||
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user