Skip to content

Commit

Permalink
add TemplateMenuItem
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephAbbey committed Jan 14, 2024
1 parent cec10bb commit 31b3078
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 9 deletions.
10 changes: 6 additions & 4 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@
"template": {
"type": "object",
"properties": {
"entity": { "$ref": "#/$defs/entity" },
"name": { "title": "Your familiar name", "type": "string" },
"content": { "title": "What to display (template)", "type": "string" },
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"const": "template"
}
},
"tap_action": { "$ref": "#/$defs/action" }
},
"required": ["name", "content", "type"],
"required": ["name", "entity", "content", "type"],
"additionalProperties": false
},
"tap": {
Expand Down Expand Up @@ -80,9 +82,9 @@
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/tap" },
{ "$ref": "#/$defs/template" },
{ "$ref": "#/$defs/toggle" },
{ "$ref": "#/$defs/template" },
{ "$ref": "#/$defs/tap" },
{ "$ref": "#/$defs/menu" }
]
}
Expand Down
1 change: 1 addition & 0 deletions resources/strings/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
<string id="TrailingSlashErr">API URL must not have a trailing slash '/'</string>
<string id="WebhookFailed">Failed to register Webhook</string>
<string id="TemplateError">Failed to render template</string>
<string id="Available" scope="glance">Available</string>
<string id="Checking" scope="glance">Checking...</string>
<string id="Unavailable" scope="glance">Unavailable</string>
Expand Down
2 changes: 1 addition & 1 deletion source/Globals.mc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ using Toybox.Lang;
class Globals {
// Enable printing of messages to the debug console (don't make this a Property
// as the messages can't be read from a watch!)
static const scDebug = false;
static const scDebug = true;
static const scAlertTimeout = 2000; // ms
static const scTapTimeout = 1000; // ms
// Time to let the existing HTTP responses get serviced after a
Expand Down
19 changes: 19 additions & 0 deletions source/HomeAssistantMenuItemFactory.mc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class HomeAssistantMenuItemFactory {
:locX => WatchUi.LAYOUT_HALIGN_CENTER,
:locY => WatchUi.LAYOUT_VALIGN_CENTER
});

mHomeAssistantService = new HomeAssistantService();
}

Expand All @@ -66,6 +67,24 @@ class HomeAssistantMenuItemFactory {
);
}

function template(
label as Lang.String or Lang.Symbol,
identifier as Lang.Object or Null,
template as Lang.String or Null,
service as Lang.String or Null,
confirm as Lang.Boolean
) as WatchUi.MenuItem {
return new HomeAssistantTemplateMenuItem(
label,
identifier,
template,
service,
confirm,
mMenuItemOptions,
mHomeAssistantService
);
}

function tap(
label as Lang.String or Lang.Symbol,
identifier as Lang.Object or Null,
Expand Down
198 changes: 198 additions & 0 deletions source/HomeAssistantTemplateMenuItem.mc
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//-----------------------------------------------------------------------------------
//
// 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:
//
// Rendering a Home Assistant Template.
//
// 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.MenuItem {
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;

function initialize(
label as Lang.String or Lang.Symbol,
identifier as Lang.Object or Null,
template as Lang.String,
service as Lang.String or Null,
confirm as Lang.Boolean,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
} or Null,
haService as HomeAssistantService
) {
WatchUi.MenuItem.initialize(
label,
null,
identifier,
options
);

mHomeAssistantService = haService;
mTemplate = template;
mService = service;
mConfirm = confirm;
}

function callService() as Void {
if (mConfirm) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm)),
WatchUi.SLIDE_IMMEDIATE
);
} else {
onConfirm();
}
}

function onConfirm() as Void {
if (mService != null) {
mHomeAssistantService.call(mIdentifier as Lang.String, mService);
}
}

// Callback function after completing the GET request to fetch the status.
// Terminate updating the toggle menu items via the chain of calls for a permanent network
// error. The ErrorView cancellation will resume the call chain.
//
function onReturnGetState(responseCode as Lang.Number, data as Lang.String) as Void {
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
}

var status = RezStrings.getUnavailable();
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
}
ErrorView.show(RezStrings.getNoPhone() + ".");
break;

case Communications.BLE_QUEUE_FULL:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
}
ErrorView.show(RezStrings.getApiFlood());
break;

case Communications.NETWORK_REQUEST_TIMED_OUT:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
}
ErrorView.show(RezStrings.getNoResponse());
break;

case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
}
ErrorView.show(RezStrings.getNoJson());
break;

case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
}
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break;

case 404:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
}
ErrorView.show(RezStrings.getApiUrlNotFound());
break;

case 400:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 400, bad request. Template error.");
}
ErrorView.show(RezStrings.getTemplateError());
break;

case 200:
status = RezStrings.getAvailable();
setSubLabel(data);
requestUpdate();
ErrorView.unShow();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
break;

default:
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
}
ErrorView.show(RezStrings.getUnhandledHttpErr() + responseCode);
}
getApp().setApiStatus(status);
}

function getState() as Void {
if (! System.getDeviceSettings().phoneConnected) {
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call.");
}
ErrorView.show(RezStrings.getNoPhone() + ".");
getApp().setApiStatus(RezStrings.getUnavailable());
} else if (! System.getDeviceSettings().connectionAvailable) {
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
}
ErrorView.show(RezStrings.getNoInternet() + ".");
getApp().setApiStatus(RezStrings.getUnavailable());
} else {
var url = Settings.getApiUrl() + "/template";
if (Globals.scDebug) {
System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
}
Communications.makeWebRequest(
url,
{
"template" => mTemplate
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
"Authorization" => "Bearer " + Settings.getApiKey()
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_TEXT_PLAIN
},
method(:onReturnGetState)
);
}
}

}
17 changes: 14 additions & 3 deletions source/HomeAssistantView.mc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
for(var i = 0; i < items.size(); i++) {
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;
Expand All @@ -62,12 +63,16 @@ class HomeAssistantView extends WatchUi.Menu2 {
confirm = false;
}
}
if (type != null && name != null && entity != null) {
if (type.equals("toggle")) {
if (type != null && name != null) {
if (type.equals("toggle") && entity != null) {
var item = HomeAssistantMenuItemFactory.create().toggle(name, entity);
addItem(item);
mListToggleItems.add(item);
} else if (type.equals("tap") && service != null) {
} else if (type.equals("template") && content != null) {
var item = HomeAssistantMenuItemFactory.create().template(name, entity, content, service, confirm);
addItem(item);
mListToggleItems.add(item);
} else if (type.equals("tap") && entity != null && service != null) {
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, service, confirm));
} else if (type.equals("group")) {
var item = HomeAssistantMenuItemFactory.create().group(items[i]);
Expand Down Expand Up @@ -155,6 +160,12 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
System.println(haItem.getLabel() + " " + haItem.getId());
}
haItem.callService();
} else if (item instanceof HomeAssistantTemplateMenuItem) {
var haItem = item as HomeAssistantTemplateMenuItem;
if (Globals.scDebug) {
System.println(haItem.getLabel() + " " + haItem.getId());
}
haItem.callService();
} else if (item instanceof HomeAssistantViewMenuItem) {
var haMenuItem = item as HomeAssistantViewMenuItem;
if (Globals.scDebug) {
Expand Down
8 changes: 7 additions & 1 deletion source/RezStrings.mc
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class RezStrings {
private static var strNoJson as Lang.String or Null;
private static var strUnhandledHttpErr as Lang.String or Null;
private static var strTrailingSlashErr as Lang.String or Null;
private static var strWebhookFailed as Lang.String or Null;
private static var strWebhookFailed as Lang.String or Null;
private static var strTemplateError as Lang.String or Null;
(:glance)
private static var strAvailable as Lang.String or Null;
(:glance)
Expand Down Expand Up @@ -100,6 +101,7 @@ class RezStrings {
strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr);
strTrailingSlashErr = WatchUi.loadResource($.Rez.Strings.TrailingSlashErr);
strWebhookFailed = WatchUi.loadResource($.Rez.Strings.WebhookFailed);
strTemplateError = WatchUi.loadResource($.Rez.Strings.TemplateError);
strAvailable = WatchUi.loadResource($.Rez.Strings.Available);
strChecking = WatchUi.loadResource($.Rez.Strings.Checking);
strUnavailable = WatchUi.loadResource($.Rez.Strings.Unavailable);
Expand Down Expand Up @@ -184,6 +186,10 @@ class RezStrings {
return strWebhookFailed;
}

static function getTemplateError() as Lang.String {
return strTemplateError;
}

static function getAvailable() as Lang.String {
return strAvailable;
}
Expand Down
23 changes: 23 additions & 0 deletions source/WebhookManager.mc
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
//-----------------------------------------------------------------------------------
//
// 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, 10 January 2024
//
//
// Description:
//
// Home Assistant Webhook creation.
//
// Reference:
// * https://developers.home-assistant.io/docs/api/native-app-integration
//
//-----------------------------------------------------------------------------------

using Toybox.Lang;
using Toybox.Communications;
using Toybox.System;
Expand Down

0 comments on commit 31b3078

Please sign in to comment.