Compare commits

...

7 Commits
v2.19 ... v2.20

Author SHA1 Message Date
2e7216b6b2 v2.20 documentation update 2024-08-30 21:18:56 +01:00
57a44d8946 Update WebhookManager.mc (#179)
Speculative fix to handle the callback data from webhook generation
perhaps not being Lang.Dict.
2024-08-30 19:48:47 +01:00
4432c7b9a0 Update WebhookManager.mc
Speculative fix to handle the callback data from webhook generation perhaps not being Lang.Dict.
2024-08-30 15:49:06 +01:00
15e2b19193 Deprecate template type (#177) 2024-08-30 14:32:22 +01:00
1b40231360 Fix errors 2024-08-30 13:49:09 +01:00
446c579660 Merge branch 'main' into 176-deprecate-template-items 2024-08-30 13:26:31 +01:00
1c182dd615 Deprecate template type 2024-08-30 13:25:16 +01:00
9 changed files with 104 additions and 204 deletions

View File

@ -32,3 +32,4 @@
| 2.17 | Bug fix for reporting activity metrics that are not found on some devices. | | 2.17 | Bug fix for reporting activity metrics that are not found on some devices. |
| 2.18 | Bug fix for reporting activity metrics that might be `null` sometimes. This is unsimulatable situation, so this version is a change based on an informed guess. | | 2.18 | Bug fix for reporting activity metrics that might be `null` sometimes. This is unsimulatable situation, so this version is a change based on an informed guess. |
| 2.19 | A template to evaluate is now optionally allowed on both `group` and `toggle` menu items. The template to evaluate is non-optional on a `template` menu item. All updates are performed in a single HTTP GET request for efficiency. Bug fix for negative heading values. Vibration now (optionally) confirms toggle menu items being tapped. | | 2.19 | A template to evaluate is now optionally allowed on both `group` and `toggle` menu items. The template to evaluate is non-optional on a `template` menu item. All updates are performed in a single HTTP GET request for efficiency. Bug fix for negative heading values. Vibration now (optionally) confirms toggle menu items being tapped. |
| 2.20 | Simplified the code base now that templates have been requested in all menu items. This means the `template` menu item became a superset of `tap`. Therefore the `tap` code has been has been upgraded to include `template` and the latter deprecated. JSON menu definitions continue to support `template` items by instantiating a `tap` menu item, but the schema marks them as deprecated and users should migrate their menu definitions now. Use the [web editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) for assistance with changes. |

View File

@ -310,3 +310,5 @@ Stack:
``` ```
The only useful information we can glean from this log is the first line, `Error: Unexpected Type Error`. There is no useful mapping to a line of code unless someone can explain to us how to use the `pc` line. Being able to send us the error type does serve as a clue. The only useful information we can glean from this log is the first line, `Error: Unexpected Type Error`. There is no useful mapping to a line of code unless someone can explain to us how to use the `pc` line. Being able to send us the error type does serve as a clue.
More on [debugging Monkey C applications](https://developer.garmin.com/connect-iq/core-topics/debugging/#appcrashes). The filenames and line numbers must only be present for deployment of code instrumented for debug.

View File

@ -64,7 +64,10 @@
}, },
"type": { "type": {
"$ref": "#/$defs/type", "$ref": "#/$defs/type",
"const": "template" "const": "template",
"deprecated": true,
"title": "Schema change:",
"description": "Use 'tap' instead."
} }
}, },
"required": ["name", "content", "type"], "required": ["name", "content", "type"],
@ -84,7 +87,10 @@
}, },
"type": { "type": {
"$ref": "#/$defs/type", "$ref": "#/$defs/type",
"const": "template" "const": "template",
"deprecated": true,
"title": "Schema change:",
"description": "Use 'tap' instead."
}, },
"tap_action": { "tap_action": {
"$ref": "#/$defs/tap_action" "$ref": "#/$defs/tap_action"
@ -108,6 +114,10 @@
"$ref": "#/$defs/type", "$ref": "#/$defs/type",
"const": "tap" "const": "tap"
}, },
"content": {
"$ref": "#/$defs/content",
"description": "Optional in a tap."
},
"service": { "service": {
"$ref": "#/$defs/entity", "$ref": "#/$defs/entity",
"deprecated": true, "deprecated": true,
@ -118,14 +128,7 @@
"$ref": "#/$defs/tap_action" "$ref": "#/$defs/tap_action"
} }
}, },
"oneOf": [ "required": ["name", "type"],
{
"required": ["name", "type", "service"]
},
{
"required": ["name", "type", "tap_action"]
}
],
"additionalProperties": false "additionalProperties": false
}, },
"group": { "group": {

View File

@ -34,7 +34,7 @@ class HomeAssistantApp extends Application.AppBase {
private var mGlanceTimer as Timer.Timer or Null; private var mGlanceTimer as Timer.Timer or Null;
private var mUpdateTimer as Timer.Timer or Null; private var mUpdateTimer as Timer.Timer or Null;
// Array initialised by onReturnFetchMenuConfig() // Array initialised by onReturnFetchMenuConfig()
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> or Null; private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> or Null;
private var mIsGlance as Lang.Boolean = false; private var mIsGlance as Lang.Boolean = false;
private var mIsApp as Lang.Boolean = false; // Or Widget 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 mUpdating as Lang.Boolean = false; // Don't start a second chain of updates

View File

@ -79,7 +79,7 @@ class HomeAssistantMenuItemFactory {
); );
} }
function template_tap( function tap(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
entity as Lang.String or Null, entity as Lang.String or Null,
template as Lang.String or Null, template as Lang.String or Null,
@ -94,57 +94,29 @@ class HomeAssistantMenuItemFactory {
data.put("entity_id", entity); data.put("entity_id", entity);
} }
} }
return new HomeAssistantTemplateMenuItem( if (service != null) {
label, return new HomeAssistantTapMenuItem(
template, label,
service, template,
confirm, service,
data, confirm,
mTapTypeIcon, data,
mMenuItemOptions, mTapTypeIcon,
mHomeAssistantService mMenuItemOptions,
); mHomeAssistantService
} );
} else {
function template_notap( return new HomeAssistantTapMenuItem(
label as Lang.String or Lang.Symbol, label,
template as Lang.String or Null template,
) as WatchUi.MenuItem { service,
return new HomeAssistantTemplateMenuItem( confirm,
label, data,
template, mInfoTypeIcon,
null, mMenuItemOptions,
false, mHomeAssistantService
null, );
mInfoTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
}
function tap(
label as Lang.String or Lang.Symbol,
entity as Lang.String or Null,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null
) as WatchUi.MenuItem {
if (entity != null) {
if (data == null) {
data = { "entity_id" => entity };
} else {
data.put("entity_id", entity);
}
} }
return new HomeAssistantTapMenuItem(
label,
service,
confirm,
data,
mTapTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
} }
function group( function group(

View File

@ -24,20 +24,22 @@ using Toybox.Graphics;
class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem { class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
private var mHomeAssistantService as HomeAssistantService; private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String; private var mTemplate as Lang.String;
private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean; private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary or Null; private var mData as Lang.Dictionary or Null;
function initialize( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
service as Lang.String or Null, template as Lang.String,
confirm as Lang.Boolean, service as Lang.String or Null,
data as Lang.Dictionary or Null, confirm as Lang.Boolean,
icon as Graphics.BitmapType or WatchUi.Drawable, data as Lang.Dictionary or Null,
options as { icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment :alignment as WatchUi.MenuItem.Alignment
} or Null, } or Null,
haService as HomeAssistantService haService as HomeAssistantService
) { ) {
WatchUi.IconMenuItem.initialize( WatchUi.IconMenuItem.initialize(
label, label,
@ -48,16 +50,37 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
); );
mHomeAssistantService = haService; mHomeAssistantService = haService;
mTemplate = template;
mService = service; mService = service;
mConfirm = confirm; mConfirm = confirm;
mData = data; mData = data;
} }
function buildTemplate() as Lang.String or Null { function hasTemplate() as Lang.Boolean {
return null; return mTemplate != null;
} }
function updateState(data as Lang.String or Null) as Void { function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
} }
function callService() as Void { function callService() as Void {
@ -68,13 +91,15 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
WatchUi.SLIDE_IMMEDIATE WatchUi.SLIDE_IMMEDIATE
); );
} else { } else {
mHomeAssistantService.call(mService, mData); onConfirm(false);
} }
} }
// NB. Parameter 'b' is ignored // NB. Parameter 'b' is ignored
function onConfirm(b as Lang.Boolean) as Void { function onConfirm(b as Lang.Boolean) as Void {
mHomeAssistantService.call(mService, mData); if (mService != null) {
mHomeAssistantService.call(mService, mData);
}
} }
} }

View File

@ -1,105 +0,0 @@
//-----------------------------------------------------------------------------------
//
// 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, 12 January 2024
//
//
// Description:
//
// Menu button that renders a Home Assistant Template, and optionally triggers a service.
//
// Reference:
// * https://developers.home-assistant.io/docs/api/rest/
// * https://www.home-assistant.io/docs/configuration/templating
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mTemplate as Lang.String;
private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment
} or Null,
haService as HomeAssistantService
) {
WatchUi.IconMenuItem.initialize(
label,
null,
null,
icon,
options
);
mHomeAssistantService = haService;
mTemplate = template;
mService = service;
mConfirm = confirm;
mData = data;
}
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function callService() as Void {
if (mConfirm) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
WatchUi.SLIDE_IMMEDIATE
);
} else {
onConfirm(false);
}
}
// NB. Parameter 'b' is ignored
function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) {
mHomeAssistantService.call(mService, mData);
}
}
}

View File

@ -63,15 +63,8 @@ class HomeAssistantView extends WatchUi.Menu2 {
if (type != null && name != null) { if (type != null && name != null) {
if (type.equals("toggle") && entity != null) { if (type.equals("toggle") && entity != null) {
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm)); addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm));
} else if (type.equals("template") && content != null) { } else if ((type.equals("tap") && service != null) || (type.equals("template") && content != null)) {
if (service == null) { addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, data));
addItem(HomeAssistantMenuItemFactory.create().template_notap(name, content));
} else {
addItem(HomeAssistantMenuItemFactory.create().template_tap(name, entity, content, service, confirm, data));
}
} else if (type.equals("tap") && service != null) {
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, service, confirm, data));
} else if (type.equals("group")) { } else if (type.equals("group")) {
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content)); addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
} }
@ -80,7 +73,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
} }
} }
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> { function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> {
var fullList = []; var fullList = [];
var lmi = mItems as Lang.Array<WatchUi.MenuItem>; var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
@ -95,8 +88,11 @@ class HomeAssistantView extends WatchUi.Menu2 {
fullList.addAll(item.getMenuView().getItemsToUpdate()); fullList.addAll(item.getMenuView().getItemsToUpdate());
} else if (item instanceof HomeAssistantToggleMenuItem) { } else if (item instanceof HomeAssistantToggleMenuItem) {
fullList.add(item); fullList.add(item);
} else if (item instanceof HomeAssistantTemplateMenuItem) { } else if (item instanceof HomeAssistantTapMenuItem) {
fullList.add(item); var tmi = item as HomeAssistantTapMenuItem;
if (tmi.hasTemplate()) {
fullList.add(item);
}
} }
} }
@ -156,10 +152,6 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
var haItem = item as HomeAssistantTapMenuItem; var haItem = item as HomeAssistantTapMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId()); // System.println(haItem.getLabel() + " " + haItem.getId());
haItem.callService(); haItem.callService();
} else if (item instanceof HomeAssistantTemplateMenuItem) {
var haItem = item as HomeAssistantTemplateMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId());
haItem.callService();
} else if (item instanceof HomeAssistantGroupMenuItem) { } else if (item instanceof HomeAssistantGroupMenuItem) {
var haMenuItem = item as HomeAssistantGroupMenuItem; var haMenuItem = item as HomeAssistantGroupMenuItem;
// System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId()); // System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId());

View File

@ -160,18 +160,28 @@ class WebhookManager {
case 200: case 200:
case 201: case 201:
var d = data as Lang.Dictionary; if (data instanceof Lang.Dictionary) {
if ((d.get("success") as Lang.Boolean or Null) != false) { var d = data as Lang.Dictionary;
if (sensors.size() == 0) { var b = d.get("success") as Lang.Boolean or Null;
getApp().startUpdates(); if (b != null and b != false) {
if (sensors.size() == 0) {
getApp().startUpdates();
} else {
registerWebhookSensor(sensors);
}
} else { } else {
registerWebhookSensor(sensors); // System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, no 'success'.");
Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
} }
} else { } else {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure"); // !! Speculative code for an application crash !!
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, not a Lang.Dict");
// Webhook ID might have been deleted on Home Assistant server and a Lang.String is trying to tell us an error message
Settings.unsetWebhookId(); Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled(); Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String); ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + data.toString());
} }
break; break;
@ -179,7 +189,7 @@ class WebhookManager {
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode); // System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
Settings.unsetWebhookId(); Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled(); Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode); ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + " " + responseCode);
} }
} }