Compare commits

...

15 Commits
v2.30 ... v2.31

Author SHA1 Message Date
6db7b67536 Update HISTORY.md
Added v2.31 text
2025-07-11 09:43:30 +01:00
e5df010af8 253 compilation failed on fenix5s (#254)
This is the fix to an annoying problem I faced as I was about to compile
the final v2.31 release. Please approve, but **don't merge** just yet.
I'll do that once the full device list compilation succeeds, just to
check if there are any residual issues.

Thanks
2025-07-11 09:31:37 +01:00
ee964ce882 Update HomeAssistantMenuItemFactory.mc
Function documentation fix.
2025-07-11 01:11:00 +01:00
ee9da24592 Fix for too many arguments to a function call on some devices 2025-07-11 01:06:15 +01:00
906cdf7371 Code documentation comments were not updated 2025-07-10 23:54:05 +01:00
7dd85937fa 243 option to close application after command (#252)
Also closes #250.
2025-07-10 22:02:12 +01:00
d141c03104 Review comment
Moved point of application exit for the menu item option.
2025-07-10 19:46:04 +01:00
842a31a1cc Review comment
enable => enabled
2025-07-10 18:24:30 +01:00
5639ff5c42 Amended heading levels in docs 2025-07-09 21:51:18 +01:00
8b3e86a00f Amended documentation for two new menu item options
The options are 'enable' and 'exit'.
2025-07-09 21:48:58 +01:00
6dbcea94cf Update HomeAssistantView.mc
Refined more precisely where the exit option can be used and enforced in the code.
2025-07-09 21:09:33 +01:00
b28daacafd Update config.schema.json
Removed exit from non-actionable menu items.
2025-07-09 20:54:28 +01:00
029a9f373e Update config.schema.json
Added to boolean options for disable and exit.
2025-07-09 19:57:11 +01:00
ec044c5408 Added two new options to menu items
1. The ability to disable a menu item without deleting it.
2. The option to quit the application on item selection.
2025-07-08 22:48:42 +01:00
659a060c76 Forgotten image used on App home page 2025-07-08 22:45:39 +01:00
11 changed files with 278 additions and 67 deletions

View File

@ -43,3 +43,4 @@
| 2.28 | Added support for Vivoactive 6 device which also required an SDK update to 8.1.0. |
| 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. |
| 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. |
| 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. |

View File

@ -47,6 +47,12 @@
}
},
"additionalProperties": false
},
"enabled": {
"$ref": "#/$defs/enabled"
},
"exit": {
"$ref": "#/$defs/exit"
}
},
"required": ["entity", "name", "type"],
@ -75,6 +81,9 @@
"deprecated": true,
"title": "Schema change:",
"description": "Use 'info' or 'tap' instead."
},
"enabled": {
"$ref": "#/$defs/enabled"
}
},
"required": ["name", "content", "type"],
@ -101,6 +110,12 @@
},
"tap_action": {
"$ref": "#/$defs/tap_action"
},
"enabled": {
"$ref": "#/$defs/enabled"
},
"exit": {
"$ref": "#/$defs/exit"
}
},
"required": ["name", "content", "type", "tap_action"],
@ -120,6 +135,9 @@
"type": {
"$ref": "#/$defs/type",
"const": "info"
},
"enabled": {
"$ref": "#/$defs/enabled"
}
},
"required": ["name", "content", "type"],
@ -149,6 +167,12 @@
},
"tap_action": {
"$ref": "#/$defs/tap_action"
},
"enabled": {
"$ref": "#/$defs/enabled"
},
"exit": {
"$ref": "#/$defs/exit"
}
},
"required": ["name", "type"],
@ -181,6 +205,9 @@
},
"items": {
"$ref": "#/$defs/items"
},
"enabled": {
"$ref": "#/$defs/enabled"
}
},
"required": ["name", "title", "type", "items"],
@ -293,6 +320,18 @@
"required": ["type"]
}
]
},
"enabled": {
"type": "boolean",
"default": true,
"title": "Enable the menu item",
"description": "Typically used to temporarily disable a menu item, e.g. for seasonal variations. Enabled (true) by default."
},
"exit": {
"type": "boolean",
"default": false,
"title": "Exit on selection",
"description": "Choose to exit the application after this item has been selected. Disabled (false) by default. N.B. Only actionable menu items can have this field added."
}
}
}

View File

@ -76,3 +76,35 @@ Note that for notify events, you _must_ not supply an `entity_id` or the API cal
> Be careful with the value of the `service` field.
Note that the `service` field will need to be a locally custom `script.<something>` as soon as any `data` fields are populated and not something more generic like `script.turn_on`. If the `service` field is wrong, the application will fail with a [`Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html) error in the response from your Home Assistant and show the error message as _"No JSON returned from HTTP request"_ on your device. In the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) you can use the standard developer tools to observe an `HTTP 400` error which the application does not see. Here we are limited by the [Garmin Connect IQ](https://developer.garmin.com/connect-iq/overview/) software development kit (SDK). We do not have enough information at the point of execution in the application to determine the cause of the error. Nor is there an immediately obvious way of identifying this issue using the JSON schema checks.
## Exit on Tap
You can choose individual items that will quit after they have completed their action.
```json
{
"entity": "automation.turn_off_stuff",
"name": "Turn off Stuff",
"type": "tap",
"tap_action": {
"service": "automation.trigger"
},
"exit": true
}
```
## Disable Menu Item
If you would like to temporarily disable an item in your menu, e.g. for seasonal reasons like not needing to turn on the heating in summer, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
```json
{
"entity": "automation.turn_off_stuff",
"name": "Turn off Stuff",
"type": "tap",
"tap_action": {
"service": "automation.trigger"
},
"enabled": false
}
```

View File

@ -109,3 +109,29 @@ Then you can use the following in your config:
"type": "toggle"
}
```
## Exit On Toggle
You can choose individual items that will quit after they have completed their action.
```json
{
"entity": "light.hall_light",
"name": "Hall Light & Quit",
"type": "toggle",
"exit": true
}
```
## Disable Menu Item
If you would like to temporarily disable an item in your menu, e.g. for seasonal reasons like not needing to turn on Christmas tree lights outside the festive season, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
```json
{
"entity": "light.chrissmas_tree",
"name": "Christmas Lights",
"type": "toggle",
"enabled": false
}
```

View File

@ -218,6 +218,19 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
}
```
## Disable Menu Item
If you would like to temporarily disable an item in your menu, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
```json
{
"name": "Phone",
"type": "info",
"content": "{{ ... }}",
"enabled": false
}
```
## Warnings
Just remember, on older smaller memory devices **you have the ability to crash the application by creating an excessive menu definition**. Templates can require significant definition for highly customised text. Don't be silly.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -70,23 +70,27 @@ class HomeAssistantMenuItemFactory {
//! @param label Menu item label.
//! @param entity_id Home Assistant Entity ID (optional)
//! @param template Template for Home Assistant to render (optional)
//! @param confirm Should this menu item selection be confirmed?
//! @param pin Should this menu item selection request the security PIN?
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//
function toggle(
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
template as Lang.String or Null,
confirm as Lang.Boolean,
pin as Lang.Boolean
options as {
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
:pin as Lang.Boolean
}
) as WatchUi.MenuItem {
var keys = mMenuItemOptions.keys();
for (var i = 0; i < keys.size(); i++) {
options.put(keys[i], mMenuItemOptions.get(keys[i]));
}
return new HomeAssistantToggleMenuItem(
label,
template,
confirm,
pin,
{ "entity_id" => entity_id },
mMenuItemOptions
options
);
}
@ -96,18 +100,20 @@ class HomeAssistantMenuItemFactory {
//! @param entity_id Home Assistant Entity ID (optional)
//! @param template Template for Home Assistant to render (optional)
//! @param service Template for Home Assistant to render (optional)
//! @param confirm Should this menu item selection be confirmed?
//! @param pin Should this menu item selection request the security PIN?
//! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
//! @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,
confirm as Lang.Boolean,
pin as Lang.Boolean,
data as Lang.Dictionary or Null
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,
options as {
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
:pin as Lang.Boolean
}
) as WatchUi.MenuItem {
if (entity_id != null) {
if (data == null) {
@ -116,28 +122,28 @@ class HomeAssistantMenuItemFactory {
data.put("entity_id", entity_id);
}
}
var keys = mMenuItemOptions.keys();
for (var i = 0; i < keys.size(); i++) {
options.put(keys[i], mMenuItemOptions.get(keys[i]));
}
if (service != null) {
options.put(:icon, mTapTypeIcon);
return new HomeAssistantTapMenuItem(
label,
template,
service,
confirm,
pin,
data,
mTapTypeIcon,
mMenuItemOptions,
options,
mHomeAssistantService
);
} else {
options.put(:icon, mInfoTypeIcon);
return new HomeAssistantTapMenuItem(
label,
template,
service,
confirm,
pin,
null,
data,
mInfoTypeIcon,
mMenuItemOptions,
options,
mHomeAssistantService
);
}

View File

@ -46,7 +46,13 @@ class HomeAssistantService {
data as Null or Lang.Dictionary or Lang.String,
context as Lang.Object
) as Void {
var entity_id = context as Lang.String or Null;
var c = context as Lang.Dictionary;
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;
}
// System.println("HomeAssistantService onReturnCall() Response Code: " + responseCode);
// System.println("HomeAssistantService onReturnCall() Response Data: " + data);
@ -102,6 +108,9 @@ class HomeAssistantService {
:bgcolor => Graphics.COLOR_BLACK
}).pushView(WatchUi.SLIDE_IMMEDIATE);
}
if (exit) {
System.exit();
}
break;
default:
@ -117,7 +126,8 @@ class HomeAssistantService {
//
function call(
service as Lang.String,
data as Lang.Dictionary or Null
data as Lang.Dictionary or Null,
exit as Lang.Boolean
) as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
@ -149,7 +159,10 @@ class HomeAssistantService {
"Authorization" => "Bearer " + Settings.getApiKey()
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
:context => entity_id
:context => {
:entity_id => entity_id,
:exit => exit
}
},
method(:onReturnCall)
);

View File

@ -21,8 +21,9 @@ 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 or Null;
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;
@ -31,45 +32,44 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//! @param label Menu item label.
//! @param template Menu item template.
//! @param service Menu item service.
//! @param data Data to supply to the service call.
//! @param exit Should the service call complete and then exit?
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security?
//! @param data Data to supply to the service call.
//! @param icon Icon to use for the menu item.
//! @param options Menu item options to be passed on.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//! @param haService Shared Home Assistant service object that will perform the required call. Only
//! 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,
confirm as Lang.Boolean,
pin as Lang.Boolean,
service as Lang.String or Null,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
: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
) {
if (options != null) {
options.put(:icon, icon);
} else {
options = { :icon => icon };
}
HomeAssistantMenuItem.initialize(
label,
template,
options
{
:alignment => options.get(:alignment),
:icon => options.get(:icon)
}
);
mHomeAssistantService = haService;
mService = service;
mConfirm = confirm;
mPin = pin;
mData = data;
mExit = options.get(:exit);
mConfirm = options.get(:confirm);
mPin = options.get(:acospin);
}
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
@ -103,7 +103,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//
function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) {
mHomeAssistantService.call(mService, mData);
mHomeAssistantService.call(mService, mData, mExit);
}
}

View File

@ -22,30 +22,30 @@ using Toybox.Timer;
//! Light or switch toggle menu button that calls the API to maintain the up to date state.
//
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mConfirm as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary;
private var mTemplate as Lang.String;
private var mExit as Lang.Boolean;
private var mConfirm as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mHasVibrate as Lang.Boolean = false;
//! Class Constructor
//!
//! @param label Menu item label.
//! @param template Menu item template.
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security?
//! @param data Data to supply to the service call.
//! @param options Menu item options to be passed on.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
confirm as Lang.Boolean,
pin as Lang.Boolean,
data as Lang.Dictionary or Null,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
: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(
@ -53,15 +53,19 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
null,
null,
false,
options
{
:alignment => options.get(:alignment),
:icon => options.get(:icon)
}
);
if (Attention has :vibrate) {
mHasVibrate = true;
}
mConfirm = confirm;
mPin = pin;
mData = data;
mTemplate = template;
mExit = options.get(:exit);
mConfirm = options.get(:confirm);
mPin = options.get(:pin);
}
//! Set the state of a toggle menu item.
@ -212,6 +216,9 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
getApp().setApiStatus(status);
if (mExit) {
System.exit();
}
}
//! Set the state of the toggle menu item.

View File

@ -51,21 +51,95 @@ class HomeAssistantView extends WatchUi.Menu2 {
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;
if (items[i].get("enabled") != null) {
enabled = items[i].get("enabled"); // Optional
}
if (items[i].get("exit") != null) {
exit = items[i].get("exit"); // Optional
}
if (tap_action != null) {
service = tap_action.get("service");
confirm = tap_action.get("confirm"); // Optional
pin = tap_action.get("pin"); // Optional
data = tap_action.get("data"); // Optional
if (confirm == null) {
confirm = false;
data = tap_action.get("data"); // Optional
if (tap_action.get("confirm") != null) {
confirm = tap_action.get("confirm"); // Optional
}
if (tap_action.get("pin") != null) {
pin = tap_action.get("pin"); // Optional
}
}
if (type != null && name != null) {
if (type != null && name != null && enabled) {
if (type.equals("toggle") && entity != null) {
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm, pin));
} else if ((type.equals("tap") && service != null) || (type.equals("info") && content != null) || (type.equals("template") && content != null)) {
addItem(HomeAssistantMenuItemFactory.create().toggle(
name,
entity,
content,
{
:exit => exit,
:confirm => confirm,
:pin => pin
}
));
} else if (type.equals("tap") && service != null) {
addItem(HomeAssistantMenuItemFactory.create().tap(
name,
entity,
content,
service,
data,
{
:exit => exit,
:confirm => confirm,
:pin => pin
}
));
} else if (type.equals("template") && content != null) {
// NB. "template" is deprecated in the schema and remains only for backward compatibility. All menu items can now use templates, so the replacement is "info".
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, pin, data));
// The exit option is dependent on the type of template.
if (tap_action == null) {
// No exit from an information only item
addItem(HomeAssistantMenuItemFactory.create().tap(
name,
entity,
content,
service,
data,
{
:exit => false,
:confirm => confirm,
:pin => pin
}
));
} else {
// You may exit from template item with a 'tap_action'.
addItem(HomeAssistantMenuItemFactory.create().tap(
name,
entity,
content,
service,
data,
{
:exit => exit,
:confirm => confirm,
:pin => pin
}
));
}
} else if (type.equals("info") && content != null) {
// Cannot exit from a non-actionable information only menu item.
addItem(HomeAssistantMenuItemFactory.create().tap(
name,
entity,
content,
service,
data,
{
:exit => false,
:confirm => confirm,
:pin => pin
}
));
} else if (type.equals("group")) {
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
}