mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-08-02 09:58:39 +00:00
add Wifi LTE command execution
This commit is contained in:
@ -14,6 +14,7 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
using Toybox.Communications;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.System;
|
||||
@ -122,11 +123,14 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
} 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.");
|
||||
} else if (! System.getDeviceSettings().phoneConnected and Settings.getWifiLteExecutionEnabled() and ! hasCachedMenu()) {
|
||||
// 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);
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("HomeAssistantApp getInitialView(): No Internet connection, skipping API call.");
|
||||
} else if (! System.getDeviceSettings().connectionAvailable and ! Settings.getWifiLteExecutionEnabled()) {
|
||||
// System.println("HomeAssistantApp getInitialView(): No Internet connection and wifi disabled, skipping API call.");
|
||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
} else {
|
||||
var isCached = fetchMenuConfig();
|
||||
@ -227,6 +231,20 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
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.
|
||||
//!
|
||||
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
|
||||
@ -246,22 +264,22 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
Settings.unsetClearCache();
|
||||
}
|
||||
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.");
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} 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;
|
||||
} 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;
|
||||
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
|
||||
} else {
|
||||
Communications.makeWebRequest(
|
||||
Settings.getConfigUrl(),
|
||||
@ -785,6 +803,13 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
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.
|
||||
|
@ -129,10 +129,25 @@ class HomeAssistantService {
|
||||
data as Lang.Dictionary or Null,
|
||||
exit as Lang.Boolean
|
||||
) 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,
|
||||
}, null),
|
||||
WatchUi.SLIDE_LEFT
|
||||
);
|
||||
} else if (! phoneConnected) {
|
||||
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
||||
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.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
} else {
|
||||
|
@ -198,16 +198,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
case 200:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
||||
getApp().forceStatusUpdates();
|
||||
var state;
|
||||
var d = data as Lang.Array;
|
||||
for(var i = 0; i < d.size(); i++) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
setToggleStateWithData(d);
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
break;
|
||||
|
||||
@ -221,23 +213,41 @@ 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) {
|
||||
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.
|
||||
//!
|
||||
//! @param s Boolean indicating the desired state of the toggle switch.
|
||||
//
|
||||
function setState(s as Lang.Boolean) as Void {
|
||||
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
|
||||
// Note: with Zigbee2MQTT a.o. we may not always get the state in the response.
|
||||
setEnabled(!isEnabled());
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
|
||||
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||
|
||||
if (! phoneConnected && ! Settings.getWifiLteExecutionEnabled()) {
|
||||
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
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.");
|
||||
// Toggle the UI back
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
} 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 url = Settings.getApiUrl() + "/services/";
|
||||
if (s) {
|
||||
@ -245,6 +255,28 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
} else {
|
||||
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
||||
}
|
||||
|
||||
if (! phoneConnected && ! internetAvailable && Settings.getWifiLteExecutionEnabled()) {
|
||||
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,
|
||||
}, {
|
||||
:confirmMethod => method(:onConfirm),
|
||||
:state => !isEnabled(),
|
||||
}),
|
||||
WatchUi.SLIDE_LEFT
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
||||
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
||||
Communications.makeWebRequest(
|
||||
@ -302,5 +334,5 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
setState(b);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ class Settings {
|
||||
private static var mCacheConfig as Lang.Boolean = false;
|
||||
private static var mClearCache as Lang.Boolean = false;
|
||||
private static var mVibrate as Lang.Boolean = false;
|
||||
private static var mWifiLteExecution as Lang.Boolean = false;
|
||||
//! seconds
|
||||
private static var mAppTimeout as Lang.Number = 0;
|
||||
//! seconds
|
||||
@ -69,6 +70,7 @@ class Settings {
|
||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
||||
mWifiLteExecution = Properties.getValue("wifi_lte_execution");
|
||||
}
|
||||
|
||||
//! A webhook is required for non-privileged API calls.
|
||||
@ -270,4 +272,12 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
//! Get the value of the WiFi/LTE toggle in settings.
|
||||
//!
|
||||
//! @return The state of the toggle.
|
||||
//
|
||||
static function getWifiLteExecutionEnabled() as Lang.Boolean {
|
||||
return mWifiLteExecution;
|
||||
}
|
||||
|
||||
}
|
||||
|
236
source/WifiLteExecution.mc
Normal file
236
source/WifiLteExecution.mc
Normal file
@ -0,0 +1,236 @@
|
||||
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 var mToggleMethod as Method(b as Lang.Boolean) as Void or Null;
|
||||
private var mToggleState as Lang.Boolean or Null;
|
||||
private var mHasToast as Lang.Boolean = false;
|
||||
private var mTimer as Timer.Timer or Null;
|
||||
|
||||
|
||||
//! 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: true to exit after running command.
|
||||
//!
|
||||
//! @param toggleItem Optional toggle state information:
|
||||
//! - confirmMethod: A method to call after confirmation.
|
||||
//! - state: The state (boolean) that will be passed to the confirmMethod.
|
||||
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,
|
||||
}, toggleItem as {
|
||||
:confirmMethod as Lang.Method,
|
||||
:state as Lang.Boolean
|
||||
} or Null) {
|
||||
if (WatchUi has :showToast) {
|
||||
mHasToast = true;
|
||||
}
|
||||
|
||||
mCommandData = {
|
||||
:type => cOptions[:type],
|
||||
:service => cOptions[:service],
|
||||
:data => cOptions[:data],
|
||||
:url => cOptions[:url],
|
||||
:callback => cOptions[:callback],
|
||||
:exit => cOptions[:exit]
|
||||
};
|
||||
if (toggleItem != null) {
|
||||
mToggleMethod = toggleItem[:confirmMethod];
|
||||
mToggleState = toggleItem[:state];
|
||||
}
|
||||
|
||||
var timeout = Settings.getConfirmTimeout(); // ms
|
||||
if (timeout > 0) {
|
||||
mTimer = new Timer.Timer();
|
||||
mTimer.start(method(:onTimeout), timeout, true);
|
||||
}
|
||||
|
||||
ConfirmationDelegate.initialize();
|
||||
}
|
||||
|
||||
//! 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 {
|
||||
if (response == WatchUi.CONFIRM_YES) {
|
||||
if (mToggleMethod != null) {
|
||||
mToggleMethod.invoke(mToggleState);
|
||||
}
|
||||
trySync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//! Initiates a bulk sync process to execute a command, if connections are available
|
||||
private function trySync() as Void {
|
||||
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
|
||||
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) {
|
||||
var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String;
|
||||
Communications.startSync2({:message => syncString});
|
||||
} 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();
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user