Moved template status updates to webhooks (#150)

This seems to work for non-privileged users.
This commit is contained in:
Joseph Abbey
2024-07-20 15:49:04 +00:00
committed by GitHub
6 changed files with 88 additions and 40 deletions

View File

@ -25,3 +25,4 @@
| 2.10 | Added a user requested feature to slow down the rate of API calls in order to reduce battery wear for a situation where the application is kept open permanently on the device for convenience. Added 4 new devices. |
| 2.11 | Bug fix release for menu caching being turned off and language corrections (Czech & Slovenian). |
| 2.12 | Re-enabled Edge 540 and Edge 840 devices which we are unable to support due to simulator issues, but the Edge 840 device has been confirmed as working by a @Petucky. |
| 2.13 | Moved the template status queries to Webhooks in order to fix the situation where an account is a non-privileged user. |

View File

@ -21,7 +21,7 @@ rem
rem -----------------------------------------------------------------------------------
rem Check this path is correct for your Java installation
set JAVA_PATH=C:\Program Files (x86)\Common Files\Oracle\Java\javapath
set JAVA_PATH=C:\Program Files\Java\jdk-22\bin
rem SDK_PATH should work for all users
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
set SDK_PATH=%SDK_PATH:~0,-1%\bin

View File

@ -204,6 +204,7 @@ class HomeAssistantApp extends Application.AppBase {
// asynchronous and affects how the views are managed.
(:glance)
function fetchMenuConfig() as Lang.Boolean {
// System.println("URL = " + Settings.getConfigUrl());
if (Settings.getConfigUrl().equals("")) {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
WatchUi.requestUpdate();
@ -257,6 +258,9 @@ class HomeAssistantApp extends Application.AppBase {
private function buildMenu(menu as Lang.Dictionary) {
mHaMenu = new HomeAssistantView(menu, null);
mQuitTimer.begin();
}
function startUpdates() {
mItemsToUpdate = mHaMenu.getItemsToUpdate();
// Start the continuous update process that continues for as long as the application is running.
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.

View File

@ -83,7 +83,7 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
// 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 {
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
@ -131,7 +131,7 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
setSubLabel(data);
setSubLabel(data.get("request"));
requestUpdate();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
@ -153,19 +153,28 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (Settings.getWebhookId().equals("")) {
getApp().updateNextMenuItem();
} else {
var url = Settings.getApiUrl() + "/template";
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// 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()
"type" => "render_template",
"data" => {
"request" => {
"template" => mTemplate
}
}
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_TEXT_PLAIN
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnGetState)
);

View File

@ -39,7 +39,7 @@ class Settings {
private static var mPollDelay as Lang.Number = 0; // seconds
private static var mConfirmTimeout as Lang.Number = 3; // seconds
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
private static var mIsBatteryLevelEnabled as Lang.Boolean = false;
private static var mIsSensorsLevelEnabled as Lang.Boolean = false;
private static var mBatteryRefreshRate as Lang.Number = 15; // minutes
private static var mIsApp as Lang.Boolean = false;
private static var mHasService as Lang.Boolean = false;
@ -60,7 +60,7 @@ class Settings {
mPollDelay = Properties.getValue("poll_delay");
mConfirmTimeout = Properties.getValue("confirm_timeout");
mMenuAlignment = Properties.getValue("menu_alignment");
mIsBatteryLevelEnabled = Properties.getValue("enable_battery_level");
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
if (System has :ServiceDelegate) {
@ -69,19 +69,40 @@ class Settings {
// Manage this inside the application or widget only (not a glance or background service process)
if (mIsApp) {
if (mIsBatteryLevelEnabled and mHasService) {
if (mHasService) {
mWebhookManager = new WebhookManager();
if (getWebhookId().equals("")) {
mWebhookManager = new WebhookManager();
// System.println("Settings update(): Doing full webhook & sensor creation.");
mWebhookManager.requestWebhookId();
} else {
// System.println("Settings update(): Doing just sensor creation.");
// We already have a Webhook ID, so just enable or disable the sensor in Home Assistant.
// Its a multiple step process, hence starting at step 0.
mWebhookManager.registerWebhookSensor({
"device_class" => "battery",
"name" => "Battery Level",
"state" => System.getSystemStats().battery,
"type" => "sensor",
"unique_id" => "battery_level",
"unit_of_measurement" => "%",
"state_class" => "measurement",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 0);
}
if ((Background.getTemporalEventRegisteredTime() == null) or
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
if (mIsSensorsLevelEnabled) {
// Create the timed activity
if ((Background.getTemporalEventRegisteredTime() == null) or
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
}
} else if (Background.getTemporalEventRegisteredTime() != null) {
Background.deleteTemporalEvent();
}
} else {
// Explicitly disable the background event which persists when the application closes.
// If !mHasService disable the Settings option as user feedback
unsetIsBatteryLevelEnabled();
unsetIsSensorsLevelEnabled();
unsetWebhookId();
}
}
@ -152,9 +173,13 @@ class Settings {
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
}
static function unsetIsBatteryLevelEnabled() {
mIsBatteryLevelEnabled = false;
Properties.setValue("enable_battery_level", mIsBatteryLevelEnabled);
static function isSensorsLevelEnabled() as Lang.Boolean {
return mIsSensorsLevelEnabled;
}
static function unsetIsSensorsLevelEnabled() {
mIsSensorsLevelEnabled = false;
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);
if (mHasService and (Background.getTemporalEventRegisteredTime() != null)) {
Background.deleteTemporalEvent();
}

View File

@ -24,6 +24,7 @@
using Toybox.Lang;
using Toybox.Communications;
using Toybox.System;
using Toybox.WatchUi;
// Can use push view so must never be run in a glance context
class WebhookManager {
@ -52,13 +53,13 @@ class WebhookManager {
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case 404:
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
@ -77,25 +78,25 @@ class WebhookManager {
"unit_of_measurement" => "%",
"state_class" => "measurement",
"entity_category" => "diagnostic",
"disabled" => false
"disabled" => !Settings.isSensorsLevelEnabled()
}, 0);
} else {
// System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
}
break;
default:
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
}
function requestWebhookId() {
// System.println("WebhookManager requestWebhookId(): Requesting webhook id");
var deviceSettings = System.getDeviceSettings();
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
Communications.makeWebRequest(
Settings.getApiUrl() + "/mobile_app/registrations",
{
@ -153,21 +154,24 @@ class WebhookManager {
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
// Webhook ID might have been deleted on Home Assistant server
Settings.unsetWebhookId();
Settings.unsetIsBatteryLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
requestWebhookId();
break;
case 404:
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: 404, page not found. Check API URL setting.");
// Webhook ID might have been deleted on Home Assistant server
Settings.unsetWebhookId();
Settings.unsetIsBatteryLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
requestWebhookId();
break;
case 200:
case 201:
if ((data.get("success") as Lang.Boolean or Null) != false) {
var d = data as Lang.Dictionary;
if ((d.get("success") as Lang.Boolean or Null) != false) {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Success");
switch (step) {
case 0:
@ -179,7 +183,7 @@ class WebhookManager {
"type" => "binary_sensor",
"unique_id" => "battery_is_charging",
"entity_category" => "diagnostic",
"disabled" => false
"disabled" => !Settings.isSensorsLevelEnabled()
}, 1);
break;
case 1:
@ -198,12 +202,12 @@ class WebhookManager {
"state" => activity,
"type" => "sensor",
"unique_id" => "activity",
"disabled" => false
"disabled" => !Settings.isSensorsLevelEnabled()
}, 2);
break;
}
case 2:
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity");
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Sub-Activity");
if (Activity has :getProfileInfo) {
var sub_activity = Activity.getProfileInfo().subSport;
if ((Activity.getActivityInfo() != null) and
@ -218,16 +222,18 @@ class WebhookManager {
"state" => sub_activity,
"type" => "sensor",
"unique_id" => "sub_activity",
"disabled" => false
"disabled" => !Settings.isSensorsLevelEnabled()
}, 3);
break;
}
case 3:
getApp().startUpdates();
default:
}
} else {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
Settings.unsetWebhookId();
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
}
break;
@ -235,15 +241,18 @@ class WebhookManager {
default:
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
Settings.unsetWebhookId();
Settings.unsetIsBatteryLevelEnabled();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
}
function registerWebhookSensor(sensor as Lang.Object, step as Lang.Number) {
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
// System.println("WebhookManager registerWebhookSensor(): URL=" + url);
// https://developers.home-assistant.io/docs/api/native-app-integration/sensors/#registering-a-sensor
Communications.makeWebRequest(
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
url,
{
"type" => "register_sensor",
"data" => sensor