Compare commits

...

19 Commits

Author SHA1 Message Date
0d73de494e Tidy 2025-07-24 20:56:08 +01:00
a686e1a104 Reordered settings 2025-07-24 19:49:44 +01:00
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
70f05e8912 Added missing code headers
And included vincentezw in headers for the code he touched.
2025-07-22 22:14:46 +01:00
c138fad6ca Update properties.xml
Moved Wi-Fi option up to be more prominent.
2025-07-22 22:05:04 +01:00
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
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
0b84983eaf use fixed poll delay from const 2025-07-21 21:02:51 +01:00
d32135af63 fix setting toggleItem, periodic pulling 2025-07-21 19:45:14 +01:00
db3fbd9886 timers as statics, defensive popviews, no double confirmation, add pin
screen onBack, toggle state tweaks
2025-07-19 10:35:35 +01:00
be7eed1ae1 early return from fetchApiStatus for in-app wifi, fix typo in docstrings 2025-07-19 10:35:35 +01:00
576f8c4a64 HomeAssistantConfirmationDelegate and HomeAssistantPinConfirmationDelegate undo toggles on timeout and reject 2025-07-19 10:35:35 +01:00
979d85fce5 show toast, not error, in updateMenuItems 2025-07-19 10:35:35 +01:00
ac899ff784 simplify delegate, toggle switch for empty response body 2025-07-19 10:35:35 +01:00
b45f02ef7b move popview up so it does not close wifi dialog 2025-07-19 10:35:35 +01:00
62f0e711c9 make setting conditional by using group 2025-07-19 10:35:35 +01:00
b2b8ffb332 separate syncdelegate class file, check for startsync2 2025-07-19 10:35:34 +01:00
172d4ad1e4 add Wifi LTE command execution 2025-07-19 10:35:34 +01:00
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.
-->
@ -53,7 +59,7 @@
Poll delay adds a user configurable delay (in seconds) to each round of
status updates of all item in the device's menu that might be amended
externally from the watch. A user has requested that it is possible to add
this delayfor an "always open" mode of operation, which then drains the
this delay for an "always open" mode of operation, which then drains the
watch battery from the additional API access activity.
-->
<property id="poll_delay_combined" type="number">5</property>
@ -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 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 static var mTimer as Timer.Timer?;
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
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) {
mTimer = new Timer.Timer();
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();
WatchUi.popView(WatchUi.SLIDE_RIGHT);
// 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,
@ -104,11 +104,11 @@ class HomeAssistantMenuItemFactory {
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//
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,
label as Lang.String or Lang.Symbol,
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
//
//-----------------------------------------------------------------------------------
@ -43,9 +43,10 @@ class PinDigit extends WatchUi.Selectable {
var height = stepY - (marginY * 2);
var button = new PinDigitButton({
:width => width,
:height => height,
:label => digit
:width => width,
:height => height,
: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
//!
@ -42,34 +42,34 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//! one of these objects is created for all menu items to re-use.
//
function initialize(
label as Lang.String or Lang.Symbol,
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) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
WatchUi.SLIDE_IMMEDIATE
);
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(
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,38 +213,62 @@ 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(
url,
mData,
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
"Authorization" => "Bearer " + Settings.getApiKey()
},
@ -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) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), b),
WatchUi.SLIDE_IMMEDIATE
);
// 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(
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