From 765d7f7f50f7237e09014f70e3636ffb2b64845b Mon Sep 17 00:00:00 2001 From: Philip Abbey Date: Sat, 11 Nov 2023 20:00:26 +0000 Subject: [PATCH 1/9] Removed timer based update mechanism Instead chaining calls from the previous update is a slightly close coupled way that might need to be refined. --- README.md | 39 +++++++++++++++++++++++---- manifest.xml | 2 +- resources-ara/strings/strings.xml | 2 +- resources-bul/strings/strings.xml | 2 +- resources-ces/strings/strings.xml | 2 +- resources-dan/strings/strings.xml | 2 +- resources-deu/strings/strings.xml | 2 +- resources-dut/strings/strings.xml | 2 +- resources-est/strings/strings.xml | 2 +- resources-fin/strings/strings.xml | 2 +- resources-fre/strings/strings.xml | 2 +- resources-gre/strings/strings.xml | 2 +- resources-heb/strings/strings.xml | 2 +- resources-hrv/strings/strings.xml | 2 +- resources-hun/strings/strings.xml | 2 +- resources-ind/strings/strings.xml | 2 +- resources-ita/strings/strings.xml | 2 +- resources-jpn/strings/strings.xml | 2 +- resources-kor/strings/strings.xml | 2 +- resources-lav/strings/strings.xml | 2 +- resources-lit/strings/strings.xml | 2 +- resources-nob/strings/strings.xml | 2 +- resources-pol/strings/strings.xml | 2 +- resources-por/strings/strings.xml | 2 +- resources-ron/strings/strings.xml | 2 +- resources-slo/strings/strings.xml | 2 +- resources-slv/strings/strings.xml | 2 +- resources-spa/strings/strings.xml | 2 +- resources-swe/strings/strings.xml | 2 +- resources-tha/strings/strings.xml | 2 +- resources-tur/strings/strings.xml | 2 +- resources-ukr/strings/strings.xml | 2 +- resources-vie/strings/strings.xml | 2 +- resources-zhs/strings/strings.xml | 2 +- resources-zht/strings/strings.xml | 2 +- resources-zsm/strings/strings.xml | 2 +- resources/strings/strings.xml | 2 +- source/Globals.mc | 8 +++--- source/HomeAssistantApp.mc | 16 +++-------- source/HomeAssistantToggleMenuItem.mc | 2 ++ 40 files changed, 79 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index d388441..c020c42 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Garmin application to provide a "dashboard" to control your devices via [Home The application is designed around a simple scrollable menu where menu items have been extended to interface with the [Home Assistant API](https://developers.home-assistant.io/docs/api/rest/), e.g. to get the status of switches or lights for display on the toggle menu item. It is possible to nest menus, so there is a menu item to open a sub-menu. This can be arbitrarily deep and nested in the format of a tree of items, although you need to consider if reaching for your phone becomes quicker to select the device what you want to control. -It is important to note that your homeassistant instance will need to be accessible via HTTPS with public SSL or all requests from the Garmin will not work. This cannot be a self-signed certificate, it must be a public certificate (You can get one for free from Let's Encrypt or you can pay for homeassistant cloud). +It is important to note that your Home Assistant instance will need to be accessible via HTTPS with public SSL or all requests from the Garmin will not work. This cannot be a self-signed certificate, it must be a public certificate (You can get one for free from [Let's Encrypt](https://letsencrypt.org/) or you can pay for [Home Assistant cloud](https://www.nabucasa.com/)). ## Application Installation @@ -15,7 +15,7 @@ Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d ## Dashboard Definition -Setup for this menu is more complicated than the Connect IQ settings menu really allows you to specify. In order to make the dashboard easily configurable and easy to change, we have provided an external mechanism for specifying the menu layout, a JSON file you write, retrieved from a URL you specify. JSON was chosen over YAML because Garmin can parse JSON HTTP GET responses into its own internal dictionary, it cannot parse YAML, hence a choice of one really. We recommend you take advantage of [Home Assistant's own web server](https://www.home-assistant.io/integrations/http/#hosting-files) to provide the JSON definition. The advantage here are: +Setup for this menu is more complicated than the Connect IQ settings menu really allows you to specify. In order to make the dashboard easily configurable and easy to change, we have provided an external mechanism for specifying the menu layout, a JSON file you write, retrieved from a URL you specify. JSON was chosen over YAML because Garmin can parse JSON HTTP GET responses into its own internal dictionary, it cannot parse YAML, hence a choice of one really. Note that JSON and YAML are essentially a 1:1 format mapping except JSON does not have comments. We recommend you take advantage of [Home Assistant's own web server](https://www.home-assistant.io/integrations/http/#hosting-files) to provide the JSON definition. The advantage here are: 1. the file is as public as you make your Home Assistant, 2. the file is editable within Home Assistant via "Studio Code Server", and @@ -76,9 +76,15 @@ Example schema as shown in the images: "type": "toggle" }, { - "entity": "switch.crnr_tbl_usbs", - "name": "Corner Table USBs", + "entity": "automation.garage_door_check", + "name": "Garage Door Check", "type": "toggle" + }, + { + "entity": "scene.tv_light", + "name": "TV Lights Scene", + "type": "tap", + "service": "scene.turn_on" } ] } @@ -86,6 +92,16 @@ Example schema as shown in the images: NB. Entity names are not real in case anyone's a hacker. +The example above illustrates how to configure: + +* Light or switch toggles +* Automation enable toggles +* Script invocation (tap) +* Service invocation, e.g. Scene setting, (tap) +* A sub-menu to open (tap) + +Possible future extensions might include specifying the alternative texts to use instead of "On" and "Off", e.g. "Locked" and "Unlocked" (but wouldn't having locks operated from your watch be a security concern ;-)) + The [schema](https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json) is checked by using a URL directly back to this GitHub source repository, so you do not need to install that file. You can just copy & paste your entity names from the YAML configuration files used to configure Home Assistant. With a submenu, there's a difference between "title" and "name". The "name" goes on the menu item, and the "title" at the head of the submenu. If your dashboard definition fails to meet the schema, the application will simply drop items with the wrong field names without warning. ## API Key Creation @@ -116,8 +132,21 @@ The application will display a 'toast' showing Home Assistant's friendly name of ## External Device Changes -Home Assistant will inevitably change the state of devices you are also controlling via your Garmin. The Garmin application does not maintain a web socket to listen for changes. Instead it must poll the Home Assistant API with your key. Therefore the application is not responsive to changes, instead there will be a delay of about 5 seconds to pick up state changes. The thinking here is that the watch application will only ever be open briefly not persistently, so the delay in picking up state changes won't be observed often for any race condition between two controllers. +Home Assistant will inevitably change the state of devices you are also controlling via your Garmin. The Garmin application does not maintain a web socket to listen for changes. Instead it must poll the Home Assistant API with your key. Therefore the application is not that responsive to changes. Instead there will be a delay of multiples of 100 ms per item whose status needs to be checked and amended. + +The per toggle item delay is caused by a queue of responses to web requests filling up a queue and giving a [Communications](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html).`BLE_QUEUE_FULL` response code. For a Venu 2 Garmin watch an API call delay of 600 ms was found to be sustainable (500 ms was still too fast). The code now chains a sequence of updates, so as one finishes it invokes the next item's update. The more items requiring a status update that you pack into your dashboard, the slower each individual item will be updated! + +The thinking here is that the watch application will only ever be open briefly not persistently, so the delay in picking up state changes won't be observed often for any race condition between two controllers. + +As a consequence of this update mechanism, if you request changes too quickly you will be notified that your device cannot keep up with the rate of API responses and you will have to dismiss the error in order to continue. The is a _feature not a bug_! ## Changes to the (JSON) Dashboard Definition When you change the JSON file defining your dashboard, you must exit the application and the reopen it. It only takes a matter of a few seconds to pick up the new definition, but it is not automatic. + +## Version History + +| Version | Comment | +|:-------:|---------| +| 1.0 | Initial release for 26 devices. | +| 1.1 | Updated for 54 more devices, 80 in total. Scene support. Added vibrate acknowledgement for tap-based menu items. Falls back to a custom visual confirmation in the absence of 'toast' and vibrate support. Bug fix for large menus needing status updates. | diff --git a/manifest.xml b/manifest.xml index 1c5790f..970fb57 100644 --- a/manifest.xml +++ b/manifest.xml @@ -21,7 +21,7 @@ Use "Monkey C: Edit Application" from the Visual Studio Code command palette to update the application attributes. --> - + - + @@ -30,4 +30,8 @@ У налаштуваннях програми немає URL-адреси API У налаштуваннях програми немає URL-адреси конфігурації Надто швидкі виклики API. Будь ласка, сповільніть свої запити. + URL не знайдено. Потенційна помилка URL-адреси API в налаштуваннях. + URL не знайдено. Потенційна помилка URL-адреси конфігурації в налаштуваннях. + Запит HTTP повернув код помилки = + URL-адреса API не повинна містити косу риску '/' \ No newline at end of file diff --git a/resources-vie/strings/strings.xml b/resources-vie/strings/strings.xml index ba6f840..20d15ff 100644 --- a/resources-vie/strings/strings.xml +++ b/resources-vie/strings/strings.xml @@ -30,4 +30,8 @@ Không có URL API trong cài đặt ứng dụng Không có URL cấu hình trong cài đặt ứng dụng Cuộc gọi API quá nhanh. Hãy làm chậm yêu cầu của bạn. + Không tìm thấy URL. Lỗi URL API tiềm ẩn trong cài đặt. + Không tìm thấy URL. Lỗi URL cấu hình tiềm ẩn trong cài đặt. + Yêu cầu HTTP trả về mã lỗi = + URL API không được có dấu gạch chéo ở cuối '/' \ No newline at end of file diff --git a/resources-zhs/strings/strings.xml b/resources-zhs/strings/strings.xml index cdbdb69..dfdee43 100644 --- a/resources-zhs/strings/strings.xml +++ b/resources-zhs/strings/strings.xml @@ -30,4 +30,8 @@ 应用程序设置中没有 API URL 应用程序设置中没有配置 URL API 调用速度太快。请放慢您的请求。 + 找不到网址。设置中可能存在 API URL 错误。 + 找不到网址。设置中可能存在配置 URL 错误。 + HTTP请求返回错误码= + API URL 不得有尾部斜杠“/” \ No newline at end of file diff --git a/resources-zht/strings/strings.xml b/resources-zht/strings/strings.xml index dd5939e..8388437 100644 --- a/resources-zht/strings/strings.xml +++ b/resources-zht/strings/strings.xml @@ -30,4 +30,8 @@ 應用程式設定中沒有 API URL 應用程式設定中沒有配置 URL API 呼叫速度太快。請放慢您的請求。 + 找不到網址。設定中可能存在 API URL 錯誤。 + 找不到網址。設定中可能存在配置 URL 錯誤。 + HTTP請求回傳錯誤碼= + API URL 不得有尾部斜線“/” \ No newline at end of file diff --git a/resources-zsm/strings/strings.xml b/resources-zsm/strings/strings.xml index e4775f2..72fd875 100644 --- a/resources-zsm/strings/strings.xml +++ b/resources-zsm/strings/strings.xml @@ -30,4 +30,8 @@ Tiada URL API dalam tetapan aplikasi Tiada URL konfigurasi dalam tetapan aplikasi Panggilan API terlalu pantas. Sila perlahankan permintaan anda. + URL tidak ditemui. Ralat URL API yang berpotensi dalam tetapan. + URL tidak ditemui. Ralat URL Konfigurasi Potensi dalam tetapan. + Permintaan HTTP mengembalikan kod ralat = + URL API tidak boleh mempunyai garis miring '/' \ No newline at end of file diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index bc91157..753dce2 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -24,4 +24,8 @@ No API URL in the application settings No configuration URL in the application settings API calls too rapid. Please slow down your requests. + URL not found. Potential API URL error in settings. + URL not found. Potential Configuration URL error in settings. + HTTP request returned error code = + API URL must not have a trailing slash '/' diff --git a/source/HomeAssistantApp.mc b/source/HomeAssistantApp.mc index 387ae37..334cfca 100644 --- a/source/HomeAssistantApp.mc +++ b/source/HomeAssistantApp.mc @@ -26,23 +26,29 @@ using Toybox.Timer; class HomeAssistantApp extends Application.AppBase { hidden var mHaMenu; - hidden var strNoApiKey as Lang.String; - hidden var strNoApiUrl as Lang.String; - hidden var strNoConfigUrl as Lang.String; - hidden var strNoInternet as Lang.String; - hidden var strNoMenu as Lang.String; - hidden var strApiFlood as Lang.String; + hidden var strNoApiKey as Lang.String; + hidden var strNoApiUrl as Lang.String; + hidden var strNoConfigUrl as Lang.String; + hidden var strNoInternet as Lang.String; + hidden var strNoMenu as Lang.String; + hidden var strApiFlood as Lang.String; + hidden var strConfigUrlNotFound as Lang.String; + hidden var strUnhandledHttpErr as Lang.String; + hidden var strTrailingSlashErr as Lang.String; hidden var mItemsToUpdate; // Array initialised by onReturnFetchMenuConfig() hidden var mNextItemToUpdate = 0; // Index into the above array function initialize() { AppBase.initialize(); - strNoApiKey = WatchUi.loadResource($.Rez.Strings.NoAPIKey); - strNoApiUrl = WatchUi.loadResource($.Rez.Strings.NoApiUrl); - strNoConfigUrl = WatchUi.loadResource($.Rez.Strings.NoConfigUrl); - strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); - strNoMenu = WatchUi.loadResource($.Rez.Strings.NoMenu); - strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strNoApiKey = WatchUi.loadResource($.Rez.Strings.NoAPIKey); + strNoApiUrl = WatchUi.loadResource($.Rez.Strings.NoApiUrl); + strNoConfigUrl = WatchUi.loadResource($.Rez.Strings.NoConfigUrl); + strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); + strNoMenu = WatchUi.loadResource($.Rez.Strings.NoMenu); + strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strConfigUrlNotFound = WatchUi.loadResource($.Rez.Strings.ConfigUrlNotFound); + strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr); + strTrailingSlashErr = WatchUi.loadResource($.Rez.Strings.TrailingSlashErr); } // onStart() is called on application start up @@ -54,19 +60,26 @@ class HomeAssistantApp extends Application.AppBase { // Return the initial view of your application here function getInitialView() as Lang.Array? { + var api_url = Properties.getValue("api_url") as Lang.String; + if ((Properties.getValue("api_key") as Lang.String).length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem Note - execScript(): No API key in the application settings."); + System.println("HomeAssistantMenuItem execScript(): No API key in the application settings."); } return [new ErrorView(strNoApiKey + "."), new ErrorDelegate()] as Lang.Array; - } else if ((Properties.getValue("api_url") as Lang.String).length() == 0) { + } else if (api_url.length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem Note - execScript(): No API URL in the application settings."); + System.println("HomeAssistantMenuItem execScript(): No API URL in the application settings."); } return [new ErrorView(strNoApiUrl + "."), new ErrorDelegate()] as Lang.Array; + } else if (api_url.substring(-1, api_url.length()).equals("/")) { + if (Globals.scDebug) { + System.println("HomeAssistantMenuItem execScript(): API URL must not have a trailing slash '/'."); + } + return [new ErrorView(strTrailingSlashErr + "."), new ErrorDelegate()] as Lang.Array; } else if ((Properties.getValue("config_url") as Lang.String).length() == 0) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem Note - execScript(): No configuration URL in the application settings."); + System.println("HomeAssistantMenuItem execScript(): No configuration URL in the application settings."); } return [new ErrorView(strNoConfigUrl + "."), new ErrorDelegate()] as Lang.Array; } else if (System.getDeviceSettings().phoneConnected && System.getDeviceSettings().connectionAvailable) { @@ -74,7 +87,7 @@ class HomeAssistantApp extends Application.AppBase { return [new WatchUi.View(), new WatchUi.BehaviorDelegate()] as Lang.Array; } else { if (Globals.scDebug) { - System.println("HomeAssistantApp Note - fetchMenuConfig(): No Internet connection, skipping API call."); + System.println("HomeAssistantApp fetchMenuConfig(): No Internet connection, skipping API call."); } return [new ErrorView(strNoInternet + "."), new ErrorDelegate()] as Lang.Array; } @@ -96,23 +109,30 @@ class HomeAssistantApp extends Application.AppBase { // Avoid pushing multiple ErrorViews WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); } + } else if (responseCode == 404) { + if (Globals.scDebug) { + System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: 404, page not found. Check Configuration URL setting."); + } + WatchUi.pushView(new ErrorView(strConfigUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); } else if (responseCode == 200) { mHaMenu = new HomeAssistantView(data, null); WatchUi.switchToView(mHaMenu, new HomeAssistantViewDelegate(), WatchUi.SLIDE_IMMEDIATE); 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. - updateNextMenuItem(); - } else if (responseCode == -300) { + if (mItemsToUpdate.size() > 0) { + updateNextMenuItem(); + } + } else if (responseCode == Communications.NETWORK_REQUEST_TIMED_OUT) { if (Globals.scDebug) { - System.println("HomeAssistantApp Note - onReturnFetchMenuConfig(): Network request timeout."); + System.println("HomeAssistantApp onReturnFetchMenuConfig(): Network request timeout."); } WatchUi.pushView(new ErrorView(strNoMenu + ". " + strNoInternet + "?"), new ErrorDelegate(), WatchUi.SLIDE_UP); } else { if (Globals.scDebug) { - System.println("HomeAssistantApp Note - onReturnFetchMenuConfig(): Configuration not found or potential validation issue."); + System.println("HomeAssistantApp onReturnFetchMenuConfig(): Unhandled HTTP response code = " + responseCode); } - WatchUi.pushView(new ErrorView(strNoMenu + " code=" + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); + WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); } } diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc index 5db50eb..bedcb82 100644 --- a/source/HomeAssistantMenuItem.mc +++ b/source/HomeAssistantMenuItem.mc @@ -24,10 +24,12 @@ using Toybox.Graphics; using Toybox.Application.Properties; class HomeAssistantMenuItem extends WatchUi.MenuItem { - hidden var mApiKey = Properties.getValue("api_key"); - hidden var strNoInternet as Lang.String; - hidden var strApiFlood as Lang.String; - hidden var mService as Lang.String; + hidden var mApiKey as Lang.String; + hidden var strNoInternet as Lang.String; + hidden var strApiFlood as Lang.String; + hidden var strApiUrlNotFound as Lang.String; + hidden var strUnhandledHttpErr as Lang.String; + hidden var mService as Lang.String; function initialize( label as Lang.String or Lang.Symbol, @@ -39,8 +41,11 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol } or Null ) { - strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); - strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); + strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strApiUrlNotFound = WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound); + strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr); + mApiKey = Properties.getValue("api_key"); mService = service; WatchUi.MenuItem.initialize( label, @@ -66,12 +71,17 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { // Avoid pushing multiple ErrorViews WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); } + } else if (responseCode == 404) { + if (Globals.scDebug) { + System.println("HomeAssistantMenuItem onReturnExecScript() Response Code: 404, page not found. Check API URL setting."); + } + WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); } else if (responseCode == 200) { var d = data as Lang.Array; for(var i = 0; i < d.size(); i++) { if ((d[i].get("entity_id") as Lang.String).equals(mIdentifier)) { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem Note - onReturnExecScript(): Correct script executed."); + System.println("HomeAssistantMenuItem onReturnExecScript(): Correct script executed."); } if (WatchUi has :showToast) { WatchUi.showToast( @@ -97,6 +107,11 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { } } } + } else { + if (Globals.scDebug) { + System.println("HomeAssistantMenuItem onReturnExecScript(): Unhandled HTTP response code = " + responseCode); + } + WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); } } @@ -142,7 +157,7 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { } } else { if (Globals.scDebug) { - System.println("HomeAssistantMenuItem Note - execScript(): No Internet connection, skipping API call."); + System.println("HomeAssistantMenuItem execScript(): No Internet connection, skipping API call."); } WatchUi.pushView(new ErrorView(strNoInternet + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); } diff --git a/source/HomeAssistantToggleMenuItem.mc b/source/HomeAssistantToggleMenuItem.mc index 73e34dc..33f13a2 100644 --- a/source/HomeAssistantToggleMenuItem.mc +++ b/source/HomeAssistantToggleMenuItem.mc @@ -24,9 +24,11 @@ using Toybox.Graphics; using Toybox.Application.Properties; class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { - hidden var mApiKey = Properties.getValue("api_key"); - hidden var strNoInternet as Lang.String; - hidden var strApiFlood as Lang.String; + hidden var mApiKey as Lang.String; + hidden var strNoInternet as Lang.String; + hidden var strApiFlood as Lang.String; + hidden var strApiUrlNotFound as Lang.String; + hidden var strUnhandledHttpErr as Lang.String; function initialize( label as Lang.String or Lang.Symbol, @@ -41,10 +43,12 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { :icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol } or Null ) { - strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); - strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strNoInternet = WatchUi.loadResource($.Rez.Strings.NoInternet); + strApiFlood = WatchUi.loadResource($.Rez.Strings.ApiFlood); + strApiUrlNotFound = WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound); + strUnhandledHttpErr = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr); + mApiKey = Properties.getValue("api_key"); WatchUi.ToggleMenuItem.initialize(label, subLabel, identifier, enabled, options); - mApiKey = Properties.getValue("api_key"); } private function setUiToggle(state as Null or Lang.String) as Void { @@ -75,6 +79,17 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { // Avoid pushing multiple ErrorViews WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); } + // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. + getApp().updateNextMenuItem(); + } else if (responseCode == 404) { + if (Globals.scDebug) { + System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting."); + } + var cw = WatchUi.getCurrentView(); + if (!(cw[0] instanceof ErrorView)) { + // Avoid pushing multiple ErrorViews + WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); + } } else if (responseCode == 200) { var state = data.get("state") as Lang.String; if (Globals.scDebug) { @@ -84,9 +99,14 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String); } setUiToggle(state); + // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. + getApp().updateNextMenuItem(); + } else { + if (Globals.scDebug) { + System.println("HomeAssistantToggleMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode); + } + WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); } - // Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. - getApp().updateNextMenuItem(); } function getState() as Void { @@ -110,7 +130,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { ); } else { if (Globals.scDebug) { - System.println("HomeAssistantToggleMenuItem Note - getState(): No Internet connection, skipping API call."); + System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call."); } WatchUi.pushView(new ErrorView(strNoInternet + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); } @@ -127,10 +147,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { if (Globals.scDebug) { System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Code: BLE_QUEUE_FULL, API calls too rapid."); } + WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); + } else if (responseCode == 404) { + if (Globals.scDebug) { + System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Code: 404, page not found. Check API URL setting."); + } var cw = WatchUi.getCurrentView(); if (!(cw[0] instanceof ErrorView)) { // Avoid pushing multiple ErrorViews - WatchUi.pushView(new ErrorView(strApiFlood), new ErrorDelegate(), WatchUi.SLIDE_UP); + WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); } } else if (responseCode == 200) { var state; @@ -144,6 +169,11 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { setUiToggle(state); } } + } else { + if (Globals.scDebug) { + System.println("HomeAssistantToggleMenuItem onReturnSetState(): Unhandled HTTP response code = " + responseCode); + } + WatchUi.pushView(new ErrorView(strUnhandledHttpErr + responseCode ), new ErrorDelegate(), WatchUi.SLIDE_UP); } } @@ -180,7 +210,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem { ); } else { if (Globals.scDebug) { - System.println("HomeAssistantToggleMenuItem Note - setState(): No Internet connection, skipping API call."); + System.println("HomeAssistantToggleMenuItem setState(): No Internet connection, skipping API call."); } WatchUi.pushView(new ErrorView(strNoInternet + "."), new ErrorDelegate(), WatchUi.SLIDE_UP); } From 3bd5361ad247b01eca7b23ba48a52695f16fd37b Mon Sep 17 00:00:00 2001 From: Philip Abbey Date: Sun, 12 Nov 2023 17:36:19 +0000 Subject: [PATCH 3/9] API Level reduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduced minimum API Level required from 3.3.0 to 3.1.0 to allow more device “part numbers” to be satisfied. Fixed an issue with one device's icon size. --- .vscode/settings.json | 6 ++++++ README.md | 4 ++-- manifest.xml | 5 +++-- monkey.jungle | 8 +++++--- .../drawables.xml | 0 .../launcher.png | Bin 3840 -> 3909 bytes 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 .vscode/settings.json rename {resources-launcher-25-25 - DELETE => resources-launcher-26-26}/drawables.xml (100%) rename {resources-launcher-25-25 - DELETE => resources-launcher-26-26}/launcher.png (68%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9c38135 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "usbs", + "Venu" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 8efdd90..a7463dc 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ A Garmin application to provide a "dashboard" to control your devices via [Home Assistant](https://www.home-assistant.io/). The application will never be as fully fledged as a Home Assistant dashboard, so it is designed to be good enough for the simple and essential things. Those things that can be activated via an on/off toggle or a tap. That should cover lights, switches, and anything requiring a single press such as an automation. For anything more complicated, e.g. thermostat, it would always be quicker and simpler to reach for your phone or tablet... or the device's own remote control! -The application is designed around a simple scrollable menu where menu items have been extended to interface with the [Home Assistant API](https://developers.home-assistant.io/docs/api/rest/), e.g. to get the status of switches or lights for display on the toggle menu item. It is possible to nest menus, so there is a menu item to open a sub-menu. This can be -arbitrarily deep and nested in the format of a tree of items, although you need to consider if reaching for your phone becomes quicker to select the device what you want to control. +The application is designed around a simple scrollable menu where menu items have been extended to interface with the [Home Assistant API](https://developers.home-assistant.io/docs/api/rest/), e.g. to get the status of switches or lights for display on the toggle menu item. It is possible to nest menus, so there is a menu item to open a sub-menu. This can be arbitrarily deep and nested in the format of a tree of items, although you need to consider if reaching for your phone becomes quicker to select the device what you want to control. It is important to note that your Home Assistant instance will need to be accessible via HTTPS with public SSL or all requests from the Garmin will not work. This cannot be a self-signed certificate, it must be a public certificate (You can get one for free from [Let's Encrypt](https://letsencrypt.org/) or you can pay for [Home Assistant cloud](https://www.nabucasa.com/)). @@ -150,3 +149,4 @@ When you change the JSON file defining your dashboard, you must exit the applica |:-------:|---------| | 1.0 | Initial release for 26 devices. | | 1.1 | Updated for 54 more devices, 80 in total. Scene support. Added vibrate acknowledgement for tap-based menu items. Falls back to a custom visual confirmation in the absence of 'toast' and vibrate support. Bug fix for large menus needing status updates. | +| 1.2 | Do not crash on zero items to update. Report unreachable URLs. Verify API URL does not have a trailing slash '/'. Increased HTTP response diagnosis. Reduced minimum API Level required from 3.3.0 to 3.1.0 to allow more device "part numbers" to be satisfied. | diff --git a/manifest.xml b/manifest.xml index 1c5790f..3f2a12b 100644 --- a/manifest.xml +++ b/manifest.xml @@ -21,7 +21,7 @@ Use "Monkey C: Edit Application" from the Visual Studio Code command palette to update the application attributes. --> - + @@ -124,6 +124,7 @@ + diff --git a/monkey.jungle b/monkey.jungle index cf3cf5b..cd12c66 100644 --- a/monkey.jungle +++ b/monkey.jungle @@ -18,8 +18,9 @@ project.manifest = manifest.xml -# Device Reference -# https://developer.garmin.com/connect-iq/reference-guides/devices-reference/ +# Device References +# * https://developer.garmin.com/connect-iq/compatible-devices/ +# * https://developer.garmin.com/connect-iq/reference-guides/devices-reference/ # # Widget launcher icon, multiple resolutions # https://forums.garmin.com/developer/connect-iq/f/discussion/255433/widget-launcher-icon-multiple-resolutions/1563305 @@ -121,7 +122,8 @@ instinct2.resourcePath = $(instinct2.resourcePath);resources-launcher-62-62;reso instinct2s.resourcePath = $(instinct2s.resourcePath);resources-launcher-54-54;resources-icons-18 # Screen Size 176x176 launcher icon size 62x62 instinct2x.resourcePath = $(instinct2x.resourcePath);resources-launcher-62-62;resources-icons-21 -instinctcrossover.resourcePath = $(instinctcrossover.resourcePath);resources-launcher-62-62;resources-icons-21 +# Screen Size 176x176 launcher icon size 26x26 +instinctcrossover.resourcePath = $(instinctcrossover.resourcePath);resources-launcher-26-26;resources-icons-21 # Screen Size 218x218 launcher icon size 30x30 legacyherocaptainmarvel.resourcePath = $(legacyherocaptainmarvel.resourcePath);resources-launcher-30-30;resources-icons-26 # Screen Size 260x260 launcher icon size 35x35 diff --git a/resources-launcher-25-25 - DELETE/drawables.xml b/resources-launcher-26-26/drawables.xml similarity index 100% rename from resources-launcher-25-25 - DELETE/drawables.xml rename to resources-launcher-26-26/drawables.xml diff --git a/resources-launcher-25-25 - DELETE/launcher.png b/resources-launcher-26-26/launcher.png similarity index 68% rename from resources-launcher-25-25 - DELETE/launcher.png rename to resources-launcher-26-26/launcher.png index 7509b0b14ff1ddbcec3b71973449ccb0e4813a20..c82ef3a7b25ae1ad394abe8c5d528331c4ca3ad5 100644 GIT binary patch delta 1209 zcmV;q1V;OS9>pFZiBL{Q4GJ0x0000DNk~Le0000Q0000Q2nGNE0I5n$&aok%3V(1( zL_t(|+N_mrXw_91z<=j|&b@cLo0>0kn|Bi@TTbUJ)O?#7O)03kh;+s+B1^@>q%y=L z0tHE>H%aMJeTjk)(oixD{32FDFC>y0WSV~1Vrw?<{oi~4FX!pQUhj14COzCxCm!T7SYIV0m&Y zh|qcMDkd@j%K~Rc0}T(IKkz4%SW#a9^F%COHels5J9y&Nw^+C9W2V$K;JIF(DtiQ8 zzQZ?S39wAW;$;FJeC{jK%71I_#>)=C-UVK`Ey$`ERzSnTj?;$2tO&@>>929sK^7VCn%?x}!0bh?B6R|fBD1vDS1CS@Ty+_pO5#u{y zQaRJ+KEQ=mprqoShifm8B>>I;%?oLT|c(s*$Fuo1UeM1SGAK14|0KR5vzp@$5-b-G8Bt!APn_FxNw;dc=$AfD}j7_h!qR6FvnpUsp zsT~I?jztRY8)fBFsA_ED!Z%;x{@sl>hKTgEiZWM1Rr0f5Sm}LgB;8{BJ!#uWa{K$l zV^RLAlvO0Cn!Tjgvws_Xp4n8ZBmNb=AQ5~dsA$ghS^U^KlG_hbS{%b#A;@LXp%0;l zAB1QV>3?tzTn;$e9&)P7a_Y~Jo)GFw9VS;+Q8{Z7m%clJ+tZCUT4~9qbmP=A*T30Q zbKk0@ZrgUC_^vS}eOVXq*{@w}zX+dP4=9&3wG&I|&!)BC=YKf*N5JXMkkjoUPSoM? z%HE_YtE{H1c5e8~nPZo|O!_xJme}d^+|2Hu*|c)jBhSD2rG&v?-YdA`8IE_wIo=hg zBgoIZF-AdT@QV-G4m^)3wR2?E=DjVJ$bxj*J!{&w>|}oP!&W&;*&!eRqW~?S1g&o0 z;fK}ufMnA>s()c=t0dnj;?lVvG0R$$c)1K=5I`)J-}nRw6*v$Mvu7O*T7h^YCWn_z ztNMj2Xjt+PPA-#1ti46a3QUg%v`lnq8SheI!oko0wdW79=*R{zQHFPcyvydYWZl%r z6ILlhTkUv~b<*cu$v_WwfX<40_`P8)*l@(~mVxP#o)nZ$}?A1RD}{g-7Y~WMg2007_Im^tCFe5I<@Z(2KLY^f XJlY(iDGpi600000NkvXXu0mjf6!1Z# delta 1139 zcmV-(1dRK|9)KPpiBL{Q4GJ0x0000DNk~Le0000P0000P2nGNE0L1BqW3eHh3V$m} zL_t(|+N_mZY*bYghQGDXnPG-0g@P?J6w25RZ3-0-BA|(&s3GBENNkjgiGgVA{ec%R z(L|!rL#Y88FgXmm*s#+owdHW8Q_1Pg%6 z6irl~7NUdYcea9T0DhawS~J16=t!hb0SAX81+u^yAU8FLDpkH2)g!7LiP`+7xV?%Z z-3spkM4?aM2Ur^c+^p~wuy9g{s#4{vRCSjsyBu3+bM3$biW~wc4fV^Cp5^pOaE0eM%?Dt;2w(j+H$6+EM0a8cC&69NoK{DD-jMSewE*fqQ|! zZZId@4kU^NpX{>LJaOO@b2E(qtlhkWov(a^aS}vf2(U!qYoKvlh+r9Z&}h`L3O)-v zR?_|EanhOjqZY2+w4LoQe}9M=7h48(D4Yh;V<07`fs#{C(V2^tX|tqUJ@|t@^V^rv zweJM!c?-rp>RnrQ^3aPPl=~c$v9fNConTURWY6b&Mi$D;`ZwO$$zl#n4k&Y3uwr; z(zX8-+2++{Br(GIdTazt`6QEIJL(Wr|7JhKYc?o>^muFyyyiqWEau3 z?(zay64X=et zpQl_2;ffFW2xi5E+<&q)#A;^qYXuZF#chAmwUX$KynH&{(#GyLKB1*`aXIm&BHv!l zb1gE&#d6zC31-&T0&wDTME4IRa0M)6lETIHNopL}u=5#ImCxVVgIY`E&bVaq#n{%G zy812KUOMt!Cc9{)OEu!T-yJsnl;V-zIK?2We2Wxb7Yj}`0KjTlbR!+B(8N=qB_J9*QPg6sCN_5MjP9_rCAzWRQ$N=j_qtUMN6us054=>GkxF!J(A>GXUX6$T+(D1^WO1002ovPDHLk FV1jvdAUXg5 From 3c7d9e8b41bbbf0b1b66a8e5d3bc89c504d0b5d9 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 13 Nov 2023 22:46:10 +0000 Subject: [PATCH 4/9] Updated translate script to use `corrected` attr --- HomeAssistant.code-workspace | 4 ---- translate.py | 20 ++++++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/HomeAssistant.code-workspace b/HomeAssistant.code-workspace index 1b7e21b..876a149 100644 --- a/HomeAssistant.code-workspace +++ b/HomeAssistant.code-workspace @@ -2,10 +2,6 @@ "folders": [ { "path": "." - }, - { - "name": "abc", - "path": "../blah/abc" } ], "settings": {} diff --git a/translate.py b/translate.py index d4592a5..2192c19 100644 --- a/translate.py +++ b/translate.py @@ -18,7 +18,7 @@ # language using Google Translate. # # Python installation: -# pip install BeautifulSoup +# pip install beautifulsoup4 # pip install deep-translator # NB. For XML formatting: # pip install lxml @@ -84,9 +84,15 @@ exceptionIds: list[str] = ["AppName", "AppVersionTitle"] titleIds: list[str] = ["setMode", "tapIcon"] i = 1 -with open("./resources/strings/strings.xml") as f: +with open("./resources/strings/strings.xml", "r") as f: c = f.read().replace('\r', '') for l in languages: + os.makedirs(f"./resources-{l[0]}/strings/", exist_ok=True) + try: + with open(f"./resources-{l[0]}/strings/strings.xml", "r") as r: + curr = BeautifulSoup(r.read().replace('\r', ''), features="xml") + except FileNotFoundError: + curr = BeautifulSoup("", features=["xml"]) print(f"{i} of {langLength}: Translating English to {l[2]}") soup = BeautifulSoup(c, features="xml") soup.find(name="strings").insert_before("\n\n") @@ -99,7 +105,13 @@ with open("./resources/strings/strings.xml") as f: for s in soup.find(name="strings").findAll(name="string"): s.insert_before(" ") - if s["id"] not in exceptionIds: + if s["id"] in exceptionIds: continue + + s_curr = curr.find(name="string", attrs={ "id": s["id"] }) + if s_curr and s_curr.has_attr("corrected") and s_curr.attrs["corrected"] == "true": + s.string = s_curr.string + s.attrs["corrected"] = "true" + else: a = GoogleTranslator(source='en', target=l[1]).translate(s.string) if s["id"] in titleIds: s.string = a.title() @@ -108,8 +120,8 @@ with open("./resources/strings/strings.xml") as f: for s in soup.find(name="strings").findAll(text=lambda text:isinstance(text, Comment)): s.insert_before(" ") s.replace_with(Comment(" " + GoogleTranslator(source='en', target=l[1]).translate(s) + " ")) + #print(str(soup)) - os.makedirs(f"./resources-{l[0]}/strings/", exist_ok=True) with open(f"./resources-{l[0]}/strings/strings.xml", "wb") as w: w.write(soup.encode("utf-8")) i += 1 From c57324f7ad95076c0cda4bece8fed68424906690 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Mon, 13 Nov 2023 23:00:57 +0000 Subject: [PATCH 5/9] Create translate.yml --- .github/workflows/translate.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/translate.yml diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml new file mode 100644 index 0000000..c9a4c88 --- /dev/null +++ b/.github/workflows/translate.yml @@ -0,0 +1,30 @@ +name: Translate +on: + push: + paths: + - "resources/strings/strings.xml" + +jobs: + translate: + runs-on: ubuntu-latest + + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # added or changed files to the repository. + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4.7.1 + + - run: | + pip install beautifulsoup4 + pip install deep-translate + pip install xml + + - run: python translate.py + + # Commit all changed files back to the repository + - uses: stefanzweifel/git-auto-commit-action@v5 From 2ccd2bfbff61122015652c91355af37738080010 Mon Sep 17 00:00:00 2001 From: Philip Abbey Date: Tue, 14 Nov 2023 08:35:38 +0000 Subject: [PATCH 6/9] Changes required for 2-tap-or-toggle-dont-work-on-venu-2 Made the 'service' field for taps mandatory. This makes scripts work for both the emulator and real devices. Provide a response for automations started with a tap, and now they can be used for both tap and toggle. --- README.md | 21 ++++++++++++- config.schema.json | 2 +- source/HomeAssistantMenuItem.mc | 53 ++++++++++++++++----------------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a7463dc..68476f6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Example schema as shown in the images: { "entity": "script.food_on_table", "name": "Food is Ready!", - "type": "tap" + "type": "tap", + "service" : "script.turn_on" }, { "entity": "light.bedside_light_switch", @@ -79,6 +80,12 @@ Example schema as shown in the images: "name": "Garage Door Check", "type": "toggle" }, + { + "entity": "automation.turn_off_usb_chargers", + "name": "Turn off USBs", + "type": "tap", + "service" : "automation.trigger" + }, { "entity": "scene.tv_light", "name": "TV Lights Scene", @@ -99,6 +106,18 @@ The example above illustrates how to configure: * Service invocation, e.g. Scene setting, (tap) * A sub-menu to open (tap) +The example JSON shows an example usage of each of these Home Assistance entity types. Presently, an automation is the only one that can be either a 'tap' or a 'toggle'. + +| HA Type | Tap | Toggle | +|------------|:---:|:------:| +| Switch | N | Y | +| Light | N | Y | +| Automation | Y | Y | +| Script | Y | N | +| Scene | Y | N | + +NB. All 'tap' items must specify a 'service' tag. + Possible future extensions might include specifying the alternative texts to use instead of "On" and "Off", e.g. "Locked" and "Unlocked" (but wouldn't having locks operated from your watch be a security concern ;-)) The [schema](https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json) is checked by using a URL directly back to this GitHub source repository, so you do not need to install that file. You can just copy & paste your entity names from the YAML configuration files used to configure Home Assistant. With a submenu, there's a difference between "title" and "name". The "name" goes on the menu item, and the "title" at the head of the submenu. If your dashboard definition fails to meet the schema, the application will simply drop items with the wrong field names without warning. diff --git a/config.schema.json b/config.schema.json index 99e59b6..1edd1e9 100644 --- a/config.schema.json +++ b/config.schema.json @@ -26,7 +26,7 @@ "type": { "const": "tap" }, "service": { "$ref": "#/$defs/entity" } }, - "required": ["entity", "name", "type"], + "required": ["entity", "name", "type", "service"], "additionalProperties": false }, "menu": { diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc index bedcb82..d72ada3 100644 --- a/source/HomeAssistantMenuItem.mc +++ b/source/HomeAssistantMenuItem.mc @@ -77,36 +77,35 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { } WatchUi.pushView(new ErrorView(strApiUrlNotFound), new ErrorDelegate(), WatchUi.SLIDE_UP); } else if (responseCode == 200) { - var d = data as Lang.Array; + if (Globals.scDebug) { + System.println("HomeAssistantMenuItem onReturnExecScript(): Service executed."); + } + var d = data as Lang.Array; + var toast = "Executed"; for(var i = 0; i < d.size(); i++) { if ((d[i].get("entity_id") as Lang.String).equals(mIdentifier)) { - if (Globals.scDebug) { - System.println("HomeAssistantMenuItem onReturnExecScript(): Correct script executed."); - } - if (WatchUi has :showToast) { - WatchUi.showToast( - (d[i].get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String, - null - ); - } - if (Attention has :vibrate) { - Attention.vibrate([ - new Attention.VibeProfile(50, 100), // On for 100ms - new Attention.VibeProfile( 0, 100), // Off for 100ms - new Attention.VibeProfile(50, 100) // On for 100ms - ]); - } - if (!(WatchUi has :showToast) && !(Attention has :vibrate)) { - new Alert({ - :timeout => Globals.scAlertTimeout, - :font => Graphics.FONT_MEDIUM, - :text => (d[i].get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String, - :fgcolor => Graphics.COLOR_WHITE, - :bgcolor => Graphics.COLOR_BLACK - }).pushView(WatchUi.SLIDE_IMMEDIATE); - } + toast = (d[i].get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String; } } + if (WatchUi has :showToast) { + WatchUi.showToast(toast, null); + } + if (Attention has :vibrate) { + Attention.vibrate([ + new Attention.VibeProfile(50, 100), // On for 100ms + new Attention.VibeProfile( 0, 100), // Off for 100ms + new Attention.VibeProfile(50, 100) // On for 100ms + ]); + } + if (!(WatchUi has :showToast) && !(Attention has :vibrate)) { + new Alert({ + :timeout => Globals.scAlertTimeout, + :font => Graphics.FONT_MEDIUM, + :text => toast, + :fgcolor => Graphics.COLOR_WHITE, + :bgcolor => Graphics.COLOR_BLACK + }).pushView(WatchUi.SLIDE_IMMEDIATE); + } } else { if (Globals.scDebug) { System.println("HomeAssistantMenuItem onReturnExecScript(): Unhandled HTTP response code = " + responseCode); @@ -144,7 +143,7 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + mService.substring(0, mService.find(".")) + "/" + mService.substring(mService.find(".")+1, null); if (Globals.scDebug) { System.println("HomeAssistantMenuItem execScript() URL=" + url); - System.println("HomeAssistantMenuItem execScript() mIdentifier=" + mIdentifier); + System.println("HomeAssistantMenuItem execScript() mService=" + mService); } Communications.makeWebRequest( url, From 33bc12d7796c092039ba132dbeec0ed818fb2a2f Mon Sep 17 00:00:00 2001 From: Philip Abbey Date: Tue, 14 Nov 2023 21:14:32 +0000 Subject: [PATCH 7/9] Update HomeAssistantMenuItem.mc Review comments. Vibrate on tap, visual confirmation on receipt of response. --- source/HomeAssistantMenuItem.mc | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/source/HomeAssistantMenuItem.mc b/source/HomeAssistantMenuItem.mc index d72ada3..0212227 100644 --- a/source/HomeAssistantMenuItem.mc +++ b/source/HomeAssistantMenuItem.mc @@ -89,15 +89,7 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { } if (WatchUi has :showToast) { WatchUi.showToast(toast, null); - } - if (Attention has :vibrate) { - Attention.vibrate([ - new Attention.VibeProfile(50, 100), // On for 100ms - new Attention.VibeProfile( 0, 100), // Off for 100ms - new Attention.VibeProfile(50, 100) // On for 100ms - ]); - } - if (!(WatchUi has :showToast) && !(Attention has :vibrate)) { + } else { new Alert({ :timeout => Globals.scAlertTimeout, :font => Graphics.FONT_MEDIUM, @@ -128,7 +120,7 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { // ERROR: venu: Cannot find symbol ':substring' on type 'PolyType'. var id = mIdentifier as Lang.String; if (mService == null) { - var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + id.substring(0, id.find(".")) + "/" + id.substring(id.find(".")+1, id.length()); + var url = (Properties.getValue("api_url") as Lang.String) + "/services/" + id.substring(0, id.find(".")) + "/" + id.substring(id.find(".")+1, null); if (Globals.scDebug) { System.println("HomeAssistantMenuItem execScript() URL=" + url); System.println("HomeAssistantMenuItem execScript() mIdentifier=" + mIdentifier); @@ -154,6 +146,13 @@ class HomeAssistantMenuItem extends WatchUi.MenuItem { method(:onReturnExecScript) ); } + if (Attention has :vibrate) { + Attention.vibrate([ + new Attention.VibeProfile(50, 100), // On for 100ms + new Attention.VibeProfile( 0, 100), // Off for 100ms + new Attention.VibeProfile(50, 100) // On for 100ms + ]); + } } else { if (Globals.scDebug) { System.println("HomeAssistantMenuItem execScript(): No Internet connection, skipping API call."); From e05799bbbca018316b563d222b9e39cc14312fed Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Tue, 14 Nov 2023 22:07:02 +0000 Subject: [PATCH 8/9] Change to a seperate corrections file --- translate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/translate.py b/translate.py index 2192c19..1ba9746 100644 --- a/translate.py +++ b/translate.py @@ -89,7 +89,7 @@ with open("./resources/strings/strings.xml", "r") as f: for l in languages: os.makedirs(f"./resources-{l[0]}/strings/", exist_ok=True) try: - with open(f"./resources-{l[0]}/strings/strings.xml", "r") as r: + with open(f"./resources-{l[0]}/strings/corrections.xml", "r") as r: curr = BeautifulSoup(r.read().replace('\r', ''), features="xml") except FileNotFoundError: curr = BeautifulSoup("", features=["xml"]) @@ -108,9 +108,8 @@ with open("./resources/strings/strings.xml", "r") as f: if s["id"] in exceptionIds: continue s_curr = curr.find(name="string", attrs={ "id": s["id"] }) - if s_curr and s_curr.has_attr("corrected") and s_curr.attrs["corrected"] == "true": + if s_curr: s.string = s_curr.string - s.attrs["corrected"] = "true" else: a = GoogleTranslator(source='en', target=l[1]).translate(s.string) if s["id"] in titleIds: From f9efb7b9b85e86287e853b6289b8b732542be071 Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Tue, 14 Nov 2023 22:10:15 +0000 Subject: [PATCH 9/9] Update translate.yml --- .github/workflows/translate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index c9a4c88..a6cbe52 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -3,6 +3,7 @@ on: push: paths: - "resources/strings/strings.xml" + - "resources-*/strings/corrections.xml" jobs: translate: