Compare commits

..

19 Commits

Author SHA1 Message Date
Philip Abbey
0d73de494e Tidy 2025-07-24 20:56:08 +01:00
Philip Abbey
a686e1a104 Reordered settings 2025-07-24 19:49:44 +01:00
Philip Abbey
8868f2152c Comments & Dictionaries
Reformatted comments to work in VSCode and converted `dict.get(:key)` to `dict[:key]` syntax as its nicer.
2025-07-24 18:54:27 +01:00
Philip Abbey
70f05e8912 Added missing code headers
And included vincentezw in headers for the code he touched.
2025-07-22 22:14:46 +01:00
Philip Abbey
c138fad6ca Update properties.xml
Moved Wi-Fi option up to be more prominent.
2025-07-22 22:05:04 +01:00
Philip Abbey
9641313492 Wifi -> Wi-Fi
Amended in presentational aspects only, not in the actual code. "Wi-Fi" is the proper noun and registered trademark.
2025-07-22 22:03:10 +01:00
Philip Abbey
e2722319a6 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.
2025-07-22 20:52:07 +01:00
Vincent Elger Zwanenburg
0b84983eaf use fixed poll delay from const 2025-07-21 21:02:51 +01:00
Vincent Elger Zwanenburg
d32135af63 fix setting toggleItem, periodic pulling 2025-07-21 19:45:14 +01:00
Vincent Elger Zwanenburg
db3fbd9886 timers as statics, defensive popviews, no double confirmation, add pin
screen onBack, toggle state tweaks
2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
be7eed1ae1 early return from fetchApiStatus for in-app wifi, fix typo in docstrings 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
576f8c4a64 HomeAssistantConfirmationDelegate and HomeAssistantPinConfirmationDelegate undo toggles on timeout and reject 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
979d85fce5 show toast, not error, in updateMenuItems 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
ac899ff784 simplify delegate, toggle switch for empty response body 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
b45f02ef7b move popview up so it does not close wifi dialog 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
62f0e711c9 make setting conditional by using group 2025-07-19 10:35:35 +01:00
Vincent Elger Zwanenburg
b2b8ffb332 separate syncdelegate class file, check for startsync2 2025-07-19 10:35:34 +01:00
Vincent Elger Zwanenburg
172d4ad1e4 add Wifi LTE command execution 2025-07-19 10:35:34 +01:00
Joseph Abbey
460f247728 Improve web: dim disabled, icons, pin 2025-07-19 07:37:05 +01:00
26 changed files with 897 additions and 230 deletions

View File

@@ -2,5 +2,8 @@
"cSpell.words": [
"usbs",
"Venu"
]
],
"files.exclude": {
"resources-*": true
}
}

View File

@@ -8,7 +8,7 @@
tested on a Venu 2 device. The source code is provided at:
https://github.com/house-of-abbey/GarminHomeAssistant.
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
P A Abbey & J D Abbey & Someone0nEarth & vincentezw, 31 October 2023
-->
@@ -38,6 +38,12 @@
-->
<property id="clear_cache" type="boolean">false</property>
<!--
Enables the SyncDelegate and prompt to send a command over Wi-Fi/LTE.
This will only show when not connected to the user's phone.
-->
<property id="wifi_lte_execution" type="boolean">false</property>
<!--
Enable notification via vibrations, typically for confirmation of actions.
-->
@@ -95,5 +101,4 @@
for trouble shooting.
-->
<property id="webhook_id" type="string"></property>
</properties>

View File

@@ -8,7 +8,7 @@
tested on a Venu 2 device. The source code is provided at:
https://github.com/house-of-abbey/GarminHomeAssistant.
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
P A Abbey & J D Abbey & Someone0nEarth & vincentezw, 31 October 2023
-->
@@ -51,6 +51,15 @@
<settingConfig type="boolean" />
</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>
<setting
propertyKey="@Properties.enable_vibration"
title="@Strings.SettingsVibration"
@@ -116,4 +125,5 @@
>
<settingConfig type="alphaNumeric" readonly="true" />
</setting>
</settings>

View File

@@ -8,7 +8,7 @@
tested on a Venu 2 device. The source code is provided at:
https://github.com/house-of-abbey/GarminHomeAssistant.
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
P A Abbey & J D Abbey & Someone0nEarth & vincentezw, 31 October 2023
-->
@@ -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 Wi-Fi or LTE available</string>
<string id="WifiLtePrompt">Execute over Wi-Fi/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">Wi-Fi/LTE execution mode.</string>
<string id="WifiLteExecutionEnable">Enable executing commands over Wi-Fi/LTE.</string>
<string id="WifiLteExecutionDescription">Allows the app to start without phone connection (when menu is cached), and prompt to execute command over Wi-Fi/LTE.</string>
</strings>

View File

@@ -35,38 +35,38 @@ class Alert extends WatchUi.View {
//! Class Constructor
//! @param params A dictionary object as follows:<br>
//! &lbrace;<br>
//! &emsp; :timeout as Lang.Number, // Timeout in millseconds<br>
//! &emsp; :font as Graphics.FontType, // Text font size<br>
//! &emsp; :text as Lang.String, // Text to display<br>
//! &emsp; :fgcolor as Graphics.ColorType, // Foreground Colour<br>
//! &emsp; :bgcolor as Graphics.ColorType // Background Colour<br>
//! &rbrace;
//! `{`<br>
//! &emsp; `:timeout as Lang.Number,` // Timeout in millseconds<br>
//! &emsp; `:font as Graphics.FontType,` // Text font size<br>
//! &emsp; `:text as Lang.String,` // Text to display<br>
//! &emsp; `:fgcolor as Graphics.ColorType,` // Foreground Colour<br>
//! &emsp; `:bgcolor as Graphics.ColorType` // Background Colour<br>
//! `}`
//
function initialize(params as Lang.Dictionary) {
View.initialize();
mText = params.get(:text) as Lang.String;
mText = params[:text] as Lang.String;
if (mText == null) {
mText = "Alert";
}
mFont = params.get(:font) as Graphics.FontType;
mFont = params[:font] as Graphics.FontType;
if (mFont == null) {
mFont = Graphics.FONT_MEDIUM;
}
mFgcolor = params.get(:fgcolor) as Graphics.ColorType;
mFgcolor = params[:fgcolor] as Graphics.ColorType;
if (mFgcolor == null) {
mFgcolor = Graphics.COLOR_BLACK;
}
mBgcolor = params.get(:bgcolor) as Graphics.ColorType;
mBgcolor = params[:bgcolor] as Graphics.ColorType;
if (mBgcolor == null) {
mBgcolor = Graphics.COLOR_WHITE;
}
mTimeout = params.get(:timeout) as Lang.Number;
mTimeout = params[:timeout] as Lang.Number;
if (mTimeout == null) {
mTimeout = 2000;
}

View File

@@ -45,10 +45,10 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
//! Called on completion of an activity.
//!
//! @param activity Specified as a Dictionary with two items.<br>
//! &lbrace;<br>
//! &emsp; :sport as Activity.Sport<br>
//! &emsp; :subSport as Activity.SubSport<br>
//! &rbrace;
//! `{`<br>
//! &emsp; `:sport as Activity.Sport`<br>
//! &emsp; `:subSport as Activity.SubSport`<br>
//! `}`
//
function onActivityCompleted(
activity as {
@@ -101,8 +101,8 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
//! @param sub_activity Activity.SubSport
//
private function doUpdate(
activity as Lang.Number or Null,
sub_activity as Lang.Number or Null
activity as Lang.Number?,
sub_activity as Lang.Number?
) {
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
var position = Position.getInfo();
@@ -154,7 +154,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
(Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String),
{
"type" => "update_location",
"data" => data,
"data" => data
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,

View File

@@ -39,7 +39,7 @@ class ErrorView extends ScalableView {
// Vertical spacing between the top of the face and the error icon
private var mErrorIconMargin as Lang.Number;
private var mErrorIcon;
private var mTextArea as WatchUi.TextArea or Null;
private var mTextArea as WatchUi.TextArea?;
private var mAntiAlias as Lang.Boolean = false;
private static var instance;
@@ -169,7 +169,7 @@ class ErrorDelegate extends WatchUi.BehaviorDelegate {
WatchUi.BehaviorDelegate.initialize();
}
//! Process the event to clear the ErrorView.
//! Handle the back button (ESC) to clear the ErrorView.
//
function onBack() as Lang.Boolean {
getApp().getQuitTimer().reset();

View File

@@ -9,11 +9,12 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld & vincentezw, 31 October 2023
//
//-----------------------------------------------------------------------------------
using Toybox.Application;
using Toybox.Communications;
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.System;
@@ -24,20 +25,24 @@ using Toybox.Timer;
//
(:glance, :background)
class HomeAssistantApp extends Application.AppBase {
private var mApiStatus as Lang.String or Null;
private var mMenuStatus as Lang.String or Null;
private var mHaMenu as HomeAssistantView or Null;
private var mGlanceTemplate as Lang.String or Null = null;
private var mGlanceText as Lang.String or Null = null;
private var mQuitTimer as QuitTimer or Null;
private var mGlanceTimer as Timer.Timer or Null;
private var mUpdateTimer as Timer.Timer or Null;
private var mApiStatus as Lang.String?;
private var mHasToast as Lang.Boolean = false;
private var mMenuStatus as Lang.String?;
private var mHaMenu as HomeAssistantView?;
private var mGlanceTemplate as Lang.String? = null;
private var mGlanceText as Lang.String? = null;
private var mQuitTimer as QuitTimer?;
private var mGlanceTimer as Timer.Timer?;
private var mUpdateTimer as Timer.Timer?;
// Array initialised by onReturnFetchMenuConfig()
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> or Null;
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem>?;
private var mIsGlance as Lang.Boolean = false;
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 Wi-Fi 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 Wi-Fi disabled, skipping API call.");
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
} else {
var isCached = fetchMenuConfig();
@@ -227,6 +236,18 @@ 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 refreshed.
//
function hasCachedMenu() as Lang.Boolean {
if (Settings.getClearCache() || !Settings.getCacheConfig()) {
return false;
}
return (Storage.getValue("menu") as Lang.Dictionary) != 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 +267,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 +322,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 +431,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 Wi-Fi/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 +587,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) {
@@ -692,10 +755,10 @@ class HomeAssistantApp extends Application.AppBase {
//! Return the optional glance text that overrides the default glance content. This
//! is derived from the glance template.
//!
//! @return A string derived from the glance template
//! @return A string derived from the glance template (or null)
//
(:glance)
function getGlanceText() as Lang.String or Null {
function getGlanceText() as Lang.String? {
return mGlanceText;
}
@@ -785,6 +848,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.

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
// P A Abbey & J D Abbey & Someone0nEarth & vincentezw, 19 November 2023
//
//-----------------------------------------------------------------------------------
@@ -35,26 +35,53 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation {
//! Delegate to respond to the confirmation request.
//
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
private static var mTimer as Timer.Timer?;
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:<br>
//! `{`<br>
//! &emsp; `:callback as Method(state as Lang.Boolean) as Void,` // Method to call on confirmation.<br>
//! &emsp; `:confirmationView as WatchUi.Confirmation,` // Confirmation the delegate is active for<br>
//! &emsp; `:state as Lang.Boolean,` // Wanted state of a toggle button.<br>
//! &emsp; `:toggle as Method(state as Lang.Boolean)?` // Optional setEnabled method to untoggle ToggleItem.<br>
//! `}`
//
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)?
}
) {
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);
}
}
//! Respond to the confirmation event.
//!
//! @param response code
//! @param response response code
//! @return Required to meet the function prototype, but the base class does not indicate a definition.
//
function onResponse(response as WatchUi.Confirm) as Lang.Boolean {
@@ -64,13 +91,27 @@ 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;
}
//! 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);
}
}
}

View File

@@ -44,12 +44,12 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
private var mTextWidth as Lang.Number = 0;
// Re-usable text items for drawing
private var mApp as HomeAssistantApp;
private var mTitle as WatchUi.Text or Null;
private var mApiText as WatchUi.Text or Null;
private var mApiStatus as WatchUi.Text or Null;
private var mMenuText as WatchUi.Text or Null;
private var mMenuStatus as WatchUi.Text or Null;
private var mGlanceContent as WatchUi.TextArea or Null;
private var mTitle as WatchUi.Text?;
private var mApiText as WatchUi.Text?;
private var mApiStatus as WatchUi.Text?;
private var mMenuText as WatchUi.Text?;
private var mMenuStatus as WatchUi.Text?;
private var mGlanceContent as WatchUi.TextArea?;
private var mAntiAlias as Lang.Boolean = false;
//! Class Constructor

View File

@@ -30,7 +30,7 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
icon as WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment
} or Null
}?
) {
if (options != null) {
options.put(:icon, icon);

View File

@@ -20,7 +20,7 @@ using Toybox.Graphics;
//! Generic menu button with an icon that optionally renders a Home Assistant Template.
//
class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String or Null;
private var mTemplate as Lang.String?;
//! Class Constructor
//!
@@ -34,13 +34,13 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
} or Null
}?
) {
WatchUi.IconMenuItem.initialize(
label,
null,
null,
options.get(:icon),
options[:icon],
options
);
mTemplate = template;
@@ -56,9 +56,9 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
//! Return the menu item's template.
//!
//! @return A string with the menu item's template definition.
//! @return A string with the menu item's template definition (or null).
//
function getTemplate() as Lang.String or Null {
function getTemplate() as Lang.String? {
return mTemplate;
}

View File

@@ -74,8 +74,8 @@ class HomeAssistantMenuItemFactory {
//
function toggle(
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
template as Lang.String or Null,
entity_id as Lang.String?,
template as Lang.String?,
options as {
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
@@ -105,10 +105,10 @@ class HomeAssistantMenuItemFactory {
//
function tap(
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
template as Lang.String or Null,
service as Lang.String or Null,
data as Lang.Dictionary or Null,
entity_id as Lang.String?,
template as Lang.String?,
service as Lang.String?,
data as Lang.Dictionary?,
options as {
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
@@ -156,7 +156,7 @@ class HomeAssistantMenuItemFactory {
//
function group(
definition as Lang.Dictionary,
template as Lang.String or Null
template as Lang.String?
) as WatchUi.MenuItem {
return new HomeAssistantGroupMenuItem(
definition,

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld & vincentezw, 31 October 2023
//
//-----------------------------------------------------------------------------------
@@ -45,7 +45,8 @@ class PinDigit extends WatchUi.Selectable {
var button = new PinDigitButton({
:width => width,
:height => height,
:label => digit
:label => digit,
:touched => false
});
var buttonTouched = new PinDigitButton({
@@ -83,16 +84,23 @@ class PinDigit extends WatchUi.Selectable {
//! Class Constructor
//!
//! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.<br>
//! &lbrace;<br>
//! &emsp; :label as Lang.Number, // The digit 0..9 to display<br>
//! &emsp; :touched as Lang.Boolean, // Should the digit be filled to indicate it has been pressed?<br>
//! `{`<br>
//! &emsp; `:label as Lang.Number,` // The digit 0..9 to display<br>
//! &emsp; `:touched as Lang.Boolean,` // Should the digit be filled to indicate it has been pressed?<br>
//! &emsp; + those required by `Drawable.initialize()`<br>
//! &rbrace;
//! ``}`
//
function initialize(options) {
function initialize(
options as {
:width as Lang.Float,
:height as Lang.Float,
:label as Lang.Number,
:touched as Lang.Boolean
}
) {
Drawable.initialize(options);
mText = options.get(:label);
mTouched = options.get(:touched);
mText = options[:label];
mTouched = options[:touched];
}
//! Draw the PIN digit button.
@@ -182,24 +190,28 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
private var mPin as Lang.String;
private var mEnteredPin as Lang.String;
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
private var mTimer as Timer.Timer or Null;
private var mTimer as Timer.Timer?;
private var mState as Lang.Boolean;
private var mFailures as PinFailures;
private var mToggleMethod as Method(state as Lang.Boolean) as Void?;
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)?,
}) {
BehaviorDelegate.initialize();
mFailures = new PinFailures();
if (mFailures.isLocked()) {
@@ -208,11 +220,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 +251,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 +298,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
if (mTimer != null) {
mTimer.stop();
}
WatchUi.popView(WatchUi.SLIDE_RIGHT);
}
@@ -304,6 +324,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
goBack();
}
//! Handle the back button (ESC)
//
function onBack() as Lang.Boolean {
goBack();
return true;
}
}
@@ -315,7 +342,7 @@ class PinFailures {
const STORAGE_KEY_LOCKED as Lang.String = "pin_locked";
private var mFailures as Lang.Array<Lang.Number>;
private var mLockedUntil as Lang.Number or Null;
private var mLockedUntil as Lang.Number?;
//! Class Constructor
//

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
// P A Abbey & J D Abbey & Someone0nEarth & vincentezw, 19 November 2023
//
//-----------------------------------------------------------------------------------
@@ -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;
}
@@ -50,8 +49,8 @@ class HomeAssistantService {
var entity_id;
var exit = false;
if (c != null) {
entity_id = c.get(:entity_id) as Lang.String;
exit = c.get(:exit) as Lang.Boolean;
entity_id = c[:entity_id] as Lang.String;
exit = c[:exit] as Lang.Boolean;
}
// System.println("HomeAssistantService onReturnCall() Response Code: " + responseCode);
// System.println("HomeAssistantService onReturnCall() Response Data: " + data);
@@ -126,13 +125,28 @@ class HomeAssistantService {
//
function call(
service as Lang.String,
data as Lang.Dictionary or Null,
data as Lang.Dictionary?,
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 {

View File

@@ -0,0 +1,139 @@
//-----------------------------------------------------------------------------------
//
// Distributed under MIT Licence
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
//
//-----------------------------------------------------------------------------------
//
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & vincentezw, 22 July 2025
//
//-----------------------------------------------------------------------------------
using Toybox.Communications;
using Toybox.Lang;
//! SyncDelegate to execute single command via POST request to the Home Assistant
//! server.
//
class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
//! Retain the last synchronisation error.
private static var syncError as Lang.String?;
//! Class Constructor
//
public function initialize() {
SyncDelegate.initialize();
}
//! Called by the system to determine if a synchronisation is needed
//
public function isSyncNeeded() as Lang.Boolean {
return true;
}
//! Called by the system when starting a bulk synchronisation.
//
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 the Home Assistant server with a given payload and URL, and calls
//! haCallback.
//!
//! @param url URL for the API call.
//! @param data Data to be supplied to the API call.
//
private function performRequest(url as Lang.String, data as Lang.Dictionary?) {
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
//!
//! @param responseCode Response code.
//! @param data Response data.
//
public function haCallback(code as Lang.Number, data as 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);
}
}

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld & vincentezw, 31 October 2023
//
//-----------------------------------------------------------------------------------
@@ -21,11 +21,11 @@ using Toybox.Graphics;
//
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String or Null;
private var mService as Lang.String?;
private var mConfirm as Lang.Boolean;
private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
private var mData as Lang.Dictionary?;
//! Class Constructor
//!
@@ -44,32 +44,32 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
service as Lang.String or Null,
data as Lang.Dictionary or Null,
service as Lang.String?,
data as Lang.Dictionary?,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
:pin as Lang.Boolean
} or Null,
}?,
haService as HomeAssistantService
) {
HomeAssistantMenuItem.initialize(
label,
template,
{
:alignment => options.get(:alignment),
:icon => options.get(:icon)
:alignment => options[:alignment],
:icon => options[:icon]
}
);
mHomeAssistantService = haService;
mService = service;
mData = data;
mExit = options.get(:exit);
mConfirm = options.get(:confirm);
mPin = options.get(:pin);
mExit = options[:exit];
mConfirm = options[:confirm];
mPin = options[: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);
}

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld & vincentezw, 31 October 2023
//
//-----------------------------------------------------------------------------------
@@ -39,14 +39,14 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
data as Lang.Dictionary or Null,
data as Lang.Dictionary?,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
:pin as Lang.Boolean
} or Null
}?
) {
WatchUi.ToggleMenuItem.initialize(
label,
@@ -54,8 +54,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
null,
false,
{
:alignment => options.get(:alignment),
:icon => options.get(:icon)
:alignment => options[:alignment],
:icon => options[:icon]
}
);
if (Attention has :vibrate) {
@@ -63,9 +63,9 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
mData = data;
mTemplate = template;
mExit = options.get(:exit);
mConfirm = options.get(:confirm);
mPin = options.get(:pin);
mExit = options[:exit];
mConfirm = options[:confirm];
mPin = options[:pin];
}
//! Set the state of a toggle menu item.
@@ -82,17 +82,17 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
//! Return the menu item's template.
//!
//! @return A string with the menu item's template definition.
//! @return A string with the menu item's template definition (or null).
//
function getTemplate() as Lang.String or Null {
function getTemplate() as Lang.String? {
return mTemplate;
}
//! Return a toggle menu item's state template.
//!
//! @return A string with the menu item's template definition.
//! @return A string with the menu item's template definition (or null).
//
function getToggleTemplate() as Lang.String or Null {
function getToggleTemplate() as Lang.String? {
return "{{states('" + mData.get("entity_id") + "')}}";
}
@@ -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 actually 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,44 @@ 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 static 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;
}
}

View File

@@ -30,7 +30,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
options as {
:focus as Lang.Number,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
} or Null
}?
) {
if (options == null) {
options = { :title => definition.get("title") as Lang.String };
@@ -42,17 +42,17 @@ class HomeAssistantView extends WatchUi.Menu2 {
var items = definition.get("items") as Lang.Array<Lang.Dictionary>;
for (var i = 0; i < items.size(); i++) {
if (items[i] instanceof(Lang.Dictionary)) {
var type = items[i].get("type") as Lang.String or Null;
var name = items[i].get("name") as Lang.String or Null;
var content = items[i].get("content") as Lang.String or Null;
var entity = items[i].get("entity") as Lang.String or Null;
var tap_action = items[i].get("tap_action") as Lang.Dictionary or Null;
var service = items[i].get("service") as Lang.String or Null; // Deprecated schema
var confirm = false as Lang.Boolean or Null;
var pin = false as Lang.Boolean or Null;
var data = null as Lang.Dictionary or Null;
var enabled = true as Lang.Boolean or Null;
var exit = false as Lang.Boolean or Null;
var type = items[i].get("type") as Lang.String?;
var name = items[i].get("name") as Lang.String?;
var content = items[i].get("content") as Lang.String?;
var entity = items[i].get("entity") as Lang.String?;
var tap_action = items[i].get("tap_action") as Lang.Dictionary?;
var service = items[i].get("service") as Lang.String?; // Deprecated schema
var confirm = false as Lang.Boolean?;
var pin = false as Lang.Boolean?;
var data = null as Lang.Dictionary?;
var enabled = true as Lang.Boolean?;
var exit = false as Lang.Boolean?;
if (items[i].get("enabled") != null) {
enabled = items[i].get("enabled"); // Optional
}
@@ -207,7 +207,7 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
mTimer = getApp().getQuitTimer();
}
//! Back button event
//! Handle the back button (ESC)
//
function onBack() {
mTimer.reset();

View File

@@ -9,7 +9,7 @@
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 23 November 2023
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld & vincentezw, 23 November 2023
//
//-----------------------------------------------------------------------------------
@@ -35,13 +35,14 @@ 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
private static var mPollDelay as Lang.Number = 0;
//! seconds
private static var mConfirmTimeout as Lang.Number = 3;
private static var mPin as Lang.String or Null = "0000";
private static var mPin as Lang.String? = "0000";
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
private static var mIsSensorsLevelEnabled as Lang.Boolean = false;
//! minutes
@@ -49,7 +50,7 @@ class Settings {
private static var mIsApp as Lang.Boolean = false;
private static var mHasService as Lang.Boolean = false;
//! Must keep the object so it doesn't get garbage collected.
private static var mWebhookManager as WebhookManager or Null;
private static var mWebhookManager as WebhookManager?;
//! Called on application start and then whenever the settings are changed.
//
@@ -61,6 +62,7 @@ class Settings {
mConfigUrl = Properties.getValue("config_url");
mCacheConfig = Properties.getValue("cache_config");
mClearCache = Properties.getValue("clear_cache");
mWifiLteExecution = Properties.getValue("wifi_lte_execution");
mVibrate = Properties.getValue("enable_vibration");
mAppTimeout = Properties.getValue("app_timeout");
mPollDelay = Properties.getValue("poll_delay_combined");
@@ -191,6 +193,18 @@ class Settings {
Properties.setValue("clear_cache", mClearCache);
}
//! Get the value of the Wi-Fi/LTE toggle in settings.
//!
//! @return The state of the toggle.
//
static function getWifiLteExecutionEnabled() as Lang.Boolean {
// Wi-Fi/LTE sync execution on a cached menu
if (!mCacheConfig) {
return false;
}
return mWifiLteExecution;
}
//! Get the vibration Boolean option supplied as part of the Settings.
//!
//! @return Boolean for whether vibration is enabled.
@@ -227,7 +241,7 @@ class Settings {
//!
//! @return The menu item security PIN.
//
static function getPin() as Lang.String or Null {
static function getPin() as Lang.String? {
return mPin;
}
@@ -235,7 +249,7 @@ class Settings {
//!
//! @return The validated 4 digit string.
//
private static function validatePin() as Lang.String or Null {
private static function validatePin() as Lang.String? {
var pin = Properties.getValue("pin");
if (pin.toNumber() == null || pin.length() != 4) {
return null;

View File

@@ -71,7 +71,7 @@ class WebhookManager {
case 200:
case 201:
var id = data.get("webhook_id") as Lang.String or Null;
var id = data.get("webhook_id") as Lang.String?;
if (id != null) {
Settings.setWebhookId(id);
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering first sensor: Battery Level");
@@ -177,7 +177,7 @@ class WebhookManager {
case 201:
if (data instanceof Lang.Dictionary) {
var d = data as Lang.Dictionary;
var b = d.get("success") as Lang.Boolean or Null;
var b = d.get("success") as Lang.Boolean?;
if (b != null and b != false) {
if (sensors.size() == 0) {
getApp().startUpdates();

View File

@@ -0,0 +1,159 @@
//-----------------------------------------------------------------------------------
//
// Distributed under MIT Licence
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
//
//-----------------------------------------------------------------------------------
//
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & vincentezw, 22 July 2025
//
//-----------------------------------------------------------------------------------
using Toybox.WatchUi;
using Toybox.System;
using Toybox.Communications;
using Toybox.Lang;
using Toybox.Timer;
//! Delegate to respond to a confirmation to execute an API request via bulk
//! synchronisation.
//
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
public static var mCommandData as {
:type as Lang.String,
:service as Lang.String?,
:data as Lang.Dictionary?,
:url as Lang.String?,
:id as Lang.Number?,
:exit as Lang.Boolean
};
private static var mTimer as Timer.Timer?;
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 execution
//!
//! @param options A dictionary describing the command to be executed:<br>
//! `{`<br>
//! &emsp; `:type: as Lang.String,` // The command type, either `"service"` or `"entity"`.<br>
//! &emsp; `:service: as Lang.String?,` // (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on").<br>
//! &emsp; `:url: as Lang.Dictionary?,` // (For type `"entity"`) The full Home Assistant entity API URL.<br>
//! &emsp; `:callback: as Lang.String?,` // (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.<br>
//! &emsp; `:data: as Lang.Method?,` // (Optional) A dictionary of data to send with the request.<br>
//! &emsp; `:exit: as Lang.Boolean,` // Boolean: if set to true: exit after running command.<br>
//! &rbrace;<br>
//! @param view The Confirmation view the delegate is active for
//
function initialize(
cOptions as {
:type as Lang.String,
:service as Lang.String?,
:data as Lang.Dictionary?,
:url as Lang.String?,
:callback as Lang.Method?,
: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) {
if (connectionInfo[keys[i]].state != System.CONNECTION_STATE_NOT_INITIALIZED) {
possibleConnection = true;
break;
}
}
}
if (possibleConnection) {
if (Communications has :startSync2) {
Communications.startSync2({
:message => WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String
});
} 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);
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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
View File

@@ -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