diff --git a/HISTORY.md b/HISTORY.md index 261d3b9..cde448e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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. | diff --git a/compile_sim.cmd b/compile_sim.cmd index 90670ec..1005efb 100644 --- a/compile_sim.cmd +++ b/compile_sim.cmd @@ -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 diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 93a8681..1e94af7 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -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. diff --git a/source/HomeAssistantTemplateMenuItem.mc b/source/HomeAssistantTemplateMenuItem.mc index 23c683e..50dae71 100644 --- a/source/HomeAssistantTemplateMenuItem.mc +++ b/source/HomeAssistantTemplateMenuItem.mc @@ -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) ); diff --git a/source/Settings.mc b/source/Settings.mc index f6c903d..067e8bf 100644 --- a/source/Settings.mc +++ b/source/Settings.mc @@ -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(); } diff --git a/source/WebhookManager.mc b/source/WebhookManager.mc index 9c69643..80017fb 100644 --- a/source/WebhookManager.mc +++ b/source/WebhookManager.mc @@ -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