mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-09-16 13:41:33 +00:00
Compare commits
16 Commits
v2.31
...
e2722319a6
Author | SHA1 | Date | |
---|---|---|---|
|
e2722319a6 | ||
|
0b84983eaf | ||
|
d32135af63 | ||
|
db3fbd9886 | ||
|
be7eed1ae1 | ||
|
576f8c4a64 | ||
|
979d85fce5 | ||
|
ac899ff784 | ||
|
b45f02ef7b | ||
|
62f0e711c9 | ||
|
b2b8ffb332 | ||
|
172d4ad1e4 | ||
|
460f247728 | ||
|
a6b4925ff7 | ||
|
3672a598fb | ||
|
f6d0916315 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -2,5 +2,8 @@
|
||||
"cSpell.words": [
|
||||
"usbs",
|
||||
"Venu"
|
||||
]
|
||||
],
|
||||
"files.exclude": {
|
||||
"resources-*": true
|
||||
}
|
||||
}
|
@@ -44,3 +44,4 @@
|
||||
| 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. |
|
||||
| 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. |
|
||||
| 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. |
|
||||
| 2.32 | Bug fix for a breaking change extracting options caused by the need to rearrange function parameters for an [annoying compiler error](https://github.com/house-of-abbey/GarminHomeAssistant/issues/253). |
|
||||
|
@@ -96,4 +96,9 @@
|
||||
-->
|
||||
<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>
|
||||
|
@@ -116,4 +116,13 @@
|
||||
>
|
||||
<settingConfig type="alphaNumeric" readonly="true" />
|
||||
</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>
|
||||
|
@@ -31,7 +31,9 @@
|
||||
<string id="NoInternet">No Internet connection.</string>
|
||||
<string id="NoJson">No JSON returned from HTTP request.</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="TimedOut">Request timed out</string>
|
||||
<string id="PinInputLocked">PIN input locked for</string>
|
||||
<string id="PotentialError">Potential Error</string>
|
||||
<string id="Seconds">seconds</string>
|
||||
@@ -42,6 +44,10 @@
|
||||
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
|
||||
<string id="WebhookFailed">Failed to register Webhook</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. -->
|
||||
<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="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="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>
|
||||
|
@@ -14,6 +14,7 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
using Toybox.Communications;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.System;
|
||||
@@ -25,6 +26,7 @@ using Toybox.Timer;
|
||||
(:glance, :background)
|
||||
class HomeAssistantApp extends Application.AppBase {
|
||||
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 mHaMenu as HomeAssistantView or 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 mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||
private var mTemplates as Lang.Dictionary = {};
|
||||
private var mNotifiedNoBle as Lang.Boolean = false;
|
||||
|
||||
private const wifiPollDelayMs = 2000;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
@@ -105,6 +110,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
mUpdateTimer = new Timer.Timer();
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
mHasToast = WatchUi has :showToast;
|
||||
Settings.update();
|
||||
|
||||
if (Settings.getApiKey().length() == 0) {
|
||||
@@ -122,11 +128,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 +236,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 +269,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(),
|
||||
@@ -301,11 +324,11 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
|
||||
//! Start the periodic menu updates for as long as the application is running.
|
||||
//
|
||||
function startUpdates() {
|
||||
function startUpdates() as Void {
|
||||
if (mHaMenu != null and !mUpdating) {
|
||||
// Start the continuous update process that continues for as long as the application is running.
|
||||
updateMenuItems();
|
||||
mUpdating = true;
|
||||
updateMenuItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,15 +433,50 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
//! Construct the GET request to update all menu items.
|
||||
//
|
||||
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.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) 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.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else {
|
||||
mNotifiedNoBle = false;
|
||||
|
||||
if (mItemsToUpdate == null or mTemplates == null) {
|
||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||
mTemplates = {};
|
||||
@@ -531,20 +589,27 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
//
|
||||
(:glance)
|
||||
function fetchApiStatus() as Void {
|
||||
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
|
||||
|
||||
// System.println("API URL = " + Settings.getApiUrl());
|
||||
if (Settings.getApiUrl().equals("")) {
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||
WatchUi.requestUpdate();
|
||||
} 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.");
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||
if (mIsGlance) {
|
||||
WatchUi.requestUpdate();
|
||||
} else {
|
||||
System.println("we here");
|
||||
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.");
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||
if (mIsGlance) {
|
||||
@@ -785,6 +850,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.
|
||||
|
@@ -35,19 +35,43 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation {
|
||||
//! Delegate to respond to the confirmation request.
|
||||
//
|
||||
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 mTimer as Timer.Timer or Null;
|
||||
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
|
||||
//!
|
||||
//! @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();
|
||||
mConfirmMethod = callback;
|
||||
mState = state;
|
||||
mConfirmMethod = options[:callback];
|
||||
mConfirmationView = options[:confirmationView];
|
||||
mState = options[:state];
|
||||
mToggleMethod = options[:toggleMethod];
|
||||
|
||||
var timeout = Settings.getConfirmTimeout(); // ms
|
||||
if (timeout > 0) {
|
||||
if (mTimer == null) {
|
||||
mTimer = new Timer.Timer();
|
||||
}
|
||||
|
||||
mTimer.start(method(:onTimeout), timeout, true);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +88,11 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||
}
|
||||
if (response == WatchUi.CONFIRM_YES) {
|
||||
mConfirmMethod.invoke(mState);
|
||||
} else {
|
||||
// Undo the toggle, if we have one
|
||||
if (mToggleMethod != null) {
|
||||
mToggleMethod.invoke(!mState);
|
||||
}
|
||||
}
|
||||
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 onTimeout() as Void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -185,21 +185,25 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
private var mTimer as Timer.Timer or Null;
|
||||
private var mState as Lang.Boolean;
|
||||
private var mFailures as PinFailures;
|
||||
private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null;
|
||||
private var mView as HomeAssistantPinConfirmationView;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param callback Method to call on confirmation.
|
||||
//! @param state Current state of a toggle button.
|
||||
//! @param pin PIN to be matched.
|
||||
//! @param view PIN confirmation view.
|
||||
//! @param options A dictionary describing the following options:
|
||||
//! - callback Method to call on confirmation.
|
||||
//! - pin PIN to be matched.
|
||||
//! - state Wanted state of a toggle button.
|
||||
//! - toggle Optional setEnabled method to untoggle ToggleItem.
|
||||
//! - view PIN confirmation view.
|
||||
//
|
||||
function initialize(
|
||||
callback as Method(state as Lang.Boolean) as Void,
|
||||
state as Lang.Boolean,
|
||||
pin as Lang.String,
|
||||
view as HomeAssistantPinConfirmationView
|
||||
) {
|
||||
function initialize(options as {
|
||||
:callback as Method(state as Lang.Boolean) as Void,
|
||||
:pin as Lang.String,
|
||||
:state as Lang.Boolean,
|
||||
:view as HomeAssistantPinConfirmationView,
|
||||
:toggleMethod as (Method(state as Lang.Boolean) as Void) or Null,
|
||||
}) {
|
||||
BehaviorDelegate.initialize();
|
||||
mFailures = new PinFailures();
|
||||
if (mFailures.isLocked()) {
|
||||
@@ -208,11 +212,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
WatchUi.loadResource($.Rez.Strings.Seconds);
|
||||
WatchUi.showToast(msg, {});
|
||||
}
|
||||
mPin = pin;
|
||||
mPin = options[:pin];
|
||||
mEnteredPin = "";
|
||||
mConfirmMethod = callback;
|
||||
mState = state;
|
||||
mView = view;
|
||||
mConfirmMethod = options[:callback];
|
||||
mState = options[:state];
|
||||
mToggleMethod = options[:toggleMethod];
|
||||
mView = options[:view];
|
||||
|
||||
resetTimer();
|
||||
}
|
||||
|
||||
@@ -237,8 +243,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
if (mTimer != null) {
|
||||
mTimer.stop();
|
||||
}
|
||||
mConfirmMethod.invoke(mState);
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
|
||||
// Set the toggle, if we have one
|
||||
if (mToggleMethod != null) {
|
||||
mToggleMethod.invoke(!mState);
|
||||
}
|
||||
mConfirmMethod.invoke(mState);
|
||||
} else {
|
||||
error();
|
||||
}
|
||||
@@ -279,6 +290,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
if (mTimer != null) {
|
||||
mTimer.stop();
|
||||
}
|
||||
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
}
|
||||
|
||||
@@ -304,6 +316,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
goBack();
|
||||
}
|
||||
|
||||
//! Handle the back button (ESC)
|
||||
//
|
||||
function onBack() as Lang.Boolean {
|
||||
goBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -27,9 +27,8 @@ class HomeAssistantService {
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
if (WatchUi has :showToast) {
|
||||
mHasToast = true;
|
||||
}
|
||||
mHasToast = WatchUi has :showToast;
|
||||
|
||||
if (Attention has :vibrate) {
|
||||
mHasVibrate = true;
|
||||
}
|
||||
@@ -129,10 +128,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,
|
||||
}, dialog),
|
||||
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 {
|
||||
|
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);
|
||||
}
|
||||
}
|
@@ -69,7 +69,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
mData = data;
|
||||
mExit = options.get(:exit);
|
||||
mConfirm = options.get(:confirm);
|
||||
mPin = options.get(:acospin);
|
||||
mPin = options.get(:pin);
|
||||
}
|
||||
|
||||
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
|
||||
@@ -82,16 +82,43 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||
WatchUi.pushView(
|
||||
pinConfirmationView,
|
||||
new HomeAssistantPinConfirmationDelegate(method(:onConfirm), false, pin, pinConfirmationView),
|
||||
new HomeAssistantPinConfirmationDelegate({
|
||||
:callback => method(:onConfirm),
|
||||
:pin => pin,
|
||||
:state => false,
|
||||
:view => pinConfirmationView,
|
||||
}),
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
}
|
||||
} 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(
|
||||
new HomeAssistantConfirmation(),
|
||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
|
||||
dialog,
|
||||
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
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onConfirm(false);
|
||||
}
|
||||
|
@@ -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,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.
|
||||
//!
|
||||
//! @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
|
||||
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) {
|
||||
url = url + id.substring(0, id.find(".")) + "/turn_on";
|
||||
} else {
|
||||
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
||||
var url = getUrl(id, s);
|
||||
|
||||
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||
// Undo the toggle
|
||||
setEnabled(!isEnabled());
|
||||
wifiPrompt(s);
|
||||
return;
|
||||
}
|
||||
|
||||
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
||||
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
||||
Communications.makeWebRequest(
|
||||
@@ -275,21 +291,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
function callService(b as Lang.Boolean) as Void {
|
||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||
if (mPin && hasTouchScreen) {
|
||||
// Undo the toggle
|
||||
setEnabled(!isEnabled());
|
||||
|
||||
var pin = Settings.getPin();
|
||||
if (pin != null) {
|
||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||
WatchUi.pushView(
|
||||
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
|
||||
);
|
||||
}
|
||||
} 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(
|
||||
new HomeAssistantConfirmation(),
|
||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), b),
|
||||
confirmationView,
|
||||
new HomeAssistantConfirmationDelegate({
|
||||
:callback => method(:onConfirm),
|
||||
:confirmationView => confirmationView,
|
||||
:state => b,
|
||||
:toggleMethod => method(:setEnabled),
|
||||
}),
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onConfirm(b);
|
||||
}
|
||||
@@ -303,4 +343,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
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 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,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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -37,7 +37,8 @@
|
||||
margin-left: 0.5em;
|
||||
filter: grayscale() invert();
|
||||
}
|
||||
.template, .info {
|
||||
.template,
|
||||
.info {
|
||||
background-image: url(../resources-icons-48/info_type.svg);
|
||||
background-size: contain;
|
||||
margin-left: 0.5em;
|
||||
@@ -63,6 +64,13 @@
|
||||
margin-left: 0.5em;
|
||||
filter: grayscale() invert();
|
||||
}
|
||||
.mdi {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
@@ -285,6 +293,10 @@
|
||||
background-color: var(--ctp-mocha-overlay1);
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css"
|
||||
defer />
|
||||
</head>
|
||||
<body>
|
||||
<div id="settings">
|
||||
@@ -441,9 +453,10 @@ http:
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script src="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script>
|
||||
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
|
||||
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
|
||||
<script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
70
web/main.js
70
web/main.js
@@ -11,7 +11,7 @@ let api_token = localStorage.getItem('api_token') ?? '';
|
||||
|
||||
/**
|
||||
* Get all entities in HomeAssistant.
|
||||
* @returns {Promise<Record<string, string>>} [id, name]
|
||||
* @returns {Promise<Record<string, { name: string, icon?: string }>>} [id, name]
|
||||
*/
|
||||
async function get_entities() {
|
||||
try {
|
||||
@@ -21,7 +21,7 @@ async function get_entities() {
|
||||
Authorization: `Bearer ${api_token}`,
|
||||
},
|
||||
mode: 'cors',
|
||||
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
|
||||
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\",\\"{{ entity.attributes.icon }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
|
||||
});
|
||||
if (res.status == 401 || res.status == 403) {
|
||||
document.querySelector('#api_token').classList.add('invalid');
|
||||
@@ -29,8 +29,16 @@ async function get_entities() {
|
||||
}
|
||||
document.querySelector('#api_url').classList.remove('invalid');
|
||||
document.querySelector('#api_token').classList.remove('invalid');
|
||||
return Object.fromEntries(await res.json());
|
||||
} catch {
|
||||
const data = {};
|
||||
for (const [id, name, icon] of await res.json()) {
|
||||
data[id] = { name };
|
||||
if (icon !== '') {
|
||||
data[id].icon = icon;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Error fetching entities:', e);
|
||||
document.querySelector('#api_url').classList.add('invalid');
|
||||
return {};
|
||||
}
|
||||
@@ -57,7 +65,8 @@ async function get_devices() {
|
||||
document.querySelector('#api_url').classList.remove('invalid');
|
||||
document.querySelector('#api_token').classList.remove('invalid');
|
||||
return Object.fromEntries(await res.json());
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('Error fetching devices:', e);
|
||||
document.querySelector('#api_url').classList.add('invalid');
|
||||
return {};
|
||||
}
|
||||
@@ -84,7 +93,8 @@ async function get_areas() {
|
||||
document.querySelector('#api_url').classList.remove('invalid');
|
||||
document.querySelector('#api_token').classList.remove('invalid');
|
||||
return Object.fromEntries(await res.json());
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('Error fetching areas:', e);
|
||||
document.querySelector('#api_url').classList.add('invalid');
|
||||
return {};
|
||||
}
|
||||
@@ -119,7 +129,8 @@ async function get_services() {
|
||||
}
|
||||
}
|
||||
return services;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('Error fetching services:', e);
|
||||
document.querySelector('#api_url').classList.add('invalid');
|
||||
return [];
|
||||
}
|
||||
@@ -370,6 +381,9 @@ async function generate_schema(entities, devices, areas, services, schema) {
|
||||
confirm: {
|
||||
$ref: '#/$defs/confirm',
|
||||
},
|
||||
pin: {
|
||||
$ref: '#/$defs/pin',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -724,6 +738,7 @@ require(['vs/editor/editor.main'], async () => {
|
||||
|
||||
var decorations = editor.createDecorationsCollection([]);
|
||||
|
||||
/** @type {monaco.editor.IMarkerData[]} */
|
||||
let markers = [];
|
||||
|
||||
const renderTemplate = editor.addCommand(
|
||||
@@ -905,6 +920,7 @@ require(['vs/editor/editor.main'], async () => {
|
||||
const ast = json.parse(model.getValue());
|
||||
const data = JSON.parse(model.getValue());
|
||||
markers = [];
|
||||
/** @type {monaco.editor.IModelDeltaDecoration[]} */
|
||||
const glyphs = [];
|
||||
async function testToggle(range, entity) {
|
||||
const res = await fetch(api_url + '/states/' + entity, {
|
||||
@@ -983,8 +999,10 @@ require(['vs/editor/editor.main'], async () => {
|
||||
* @param {import('json-ast-comments').JsonAst |
|
||||
* import('json-ast-comments').JsonProperty} node
|
||||
* @param {string[]} path
|
||||
* @param {import('json-ast-comments').JsonAst |
|
||||
* import('json-ast-comments').JsonProperty | null} parent
|
||||
*/
|
||||
function recurse(node, path) {
|
||||
function recurse(node, path, parent = null) {
|
||||
if (node.type === 'property') {
|
||||
if (node.key[0].value === 'content') {
|
||||
templates.push([
|
||||
@@ -1010,11 +1028,39 @@ require(['vs/editor/editor.main'], async () => {
|
||||
}
|
||||
trim++;
|
||||
markers.push({
|
||||
message: entities[node.value[0].value] ?? 'Entity not found',
|
||||
message: entities[node.value[0].value].name ?? 'Entity not found',
|
||||
severity: monaco.MarkerSeverity.Hint,
|
||||
...range,
|
||||
startColumn: trim,
|
||||
});
|
||||
glyphs.push({
|
||||
range,
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
glyphMarginClassName:
|
||||
'mdi ' +
|
||||
entities[node.value[0].value]?.icon?.replace(':', '-'),
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
node.key[0].value === 'enabled' &&
|
||||
node.value[0].type === 'boolean' &&
|
||||
!node.value[0].value
|
||||
) {
|
||||
glyphs.push({
|
||||
range: {
|
||||
startLineNumber: parent.members[0].key[0].range.start.line + 1,
|
||||
startColumn: 0,
|
||||
endLineNumber:
|
||||
parent.members[parent.members.length - 1].value[0].range.end
|
||||
.line + 1,
|
||||
endColumn: 10000,
|
||||
},
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
inlineClassName: 'disabled',
|
||||
},
|
||||
});
|
||||
} else if (node.key[0].value === 'type') {
|
||||
if (node.value[0].value === 'toggle') {
|
||||
toggles.push([
|
||||
@@ -1041,15 +1087,15 @@ require(['vs/editor/editor.main'], async () => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
recurse(node.value[0], [...path, node.key[0].value]);
|
||||
recurse(node.value[0], [...path, node.key[0].value], node);
|
||||
}
|
||||
} else if (node.type === 'array') {
|
||||
for (let i = 0; i < node.members.length; i++) {
|
||||
recurse(node.members[i], [...path, i]);
|
||||
recurse(node.members[i], [...path, i], node);
|
||||
}
|
||||
} else if (node.type === 'object') {
|
||||
for (let member of node.members) {
|
||||
recurse(member, path);
|
||||
recurse(member, path, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,10 +10,11 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/toastify-js": "^1.12.3",
|
||||
"@types/toastify-js": "^1.12.4",
|
||||
"@vscode/webview-ui-toolkit": "1.4.0",
|
||||
"json-ast-comments": "1.1.1",
|
||||
"monaco-editor": "0.45.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"prettier": "^3.6.2",
|
||||
"serve": "^14.2.1"
|
||||
}
|
||||
}
|
25
web/pnpm-lock.yaml
generated
25
web/pnpm-lock.yaml
generated
@@ -6,8 +6,8 @@ settings:
|
||||
|
||||
devDependencies:
|
||||
'@types/toastify-js':
|
||||
specifier: ^1.12.3
|
||||
version: 1.12.3
|
||||
specifier: ^1.12.4
|
||||
version: 1.12.4
|
||||
'@vscode/webview-ui-toolkit':
|
||||
specifier: 1.4.0
|
||||
version: 1.4.0(react@18.2.0)
|
||||
@@ -15,8 +15,11 @@ devDependencies:
|
||||
specifier: 1.1.1
|
||||
version: 1.1.1
|
||||
monaco-editor:
|
||||
specifier: 0.45.0
|
||||
version: 0.45.0
|
||||
specifier: 0.52.2
|
||||
version: 0.52.2
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
serve:
|
||||
specifier: ^14.2.1
|
||||
version: 14.2.1
|
||||
@@ -52,8 +55,8 @@ packages:
|
||||
exenv-es6: 1.1.1
|
||||
dev: true
|
||||
|
||||
/@types/toastify-js@1.12.3:
|
||||
resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==}
|
||||
/@types/toastify-js@1.12.4:
|
||||
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
|
||||
dev: true
|
||||
|
||||
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
|
||||
@@ -415,8 +418,8 @@ packages:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: true
|
||||
|
||||
/monaco-editor@0.45.0:
|
||||
resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==}
|
||||
/monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
dev: true
|
||||
|
||||
/ms@2.0.0:
|
||||
@@ -460,6 +463,12 @@ packages:
|
||||
resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
|
||||
dev: true
|
||||
|
||||
/prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/punycode@1.4.1:
|
||||
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
||||
dev: true
|
||||
|
Reference in New Issue
Block a user