278 add stale while revalidate to caching (#280)

Afetr startup and setting the meni item states, check to see if the
cache menu is out of date wrt the URL and then save the new version and
prompt the user to restart the application on the device. Initial
testing of the Beta version on my watch looks good.
This commit is contained in:
__JosephAbbey
2025-09-06 16:13:56 +01:00
committed by GitHub
40 changed files with 281 additions and 45 deletions

View File

@@ -48,3 +48,4 @@
| 3.0 | First version with the ability to use [Wi-Fi or LTE](Wi-Fi.md) instead of Bluetooth but with limited functionality, thanks to [@vincentezw](https://github.com/vincentezw). |
| 3.1 | Added the ability for users to provide [custom HTTP headers](HTTP_Headers.md) for their Home Assistant server. Improved German language translations. Thanks to [@tispokes](https://github.com/tispokes) for assisting with both of those. Removed all groups in settings as the SDK is buggy. Fixed a bug with templates in glances causing application crash on startup. |
| 3.2 | Only enable or disable sensors on Home Assistant when the background service options is changed, i.e. do not call the API to enable on start up every time. |
| 3.3 | Providing automatic detection for menu definition updates, but still requires an application restart. |

View File

@@ -260,9 +260,12 @@ Unfortunately the Settings dialogue box in the Garmin IQ application "times out"
You should now have a working application on your watch and be able to operate your Home Assistant devices for as long as your watch is within Bluetooth range of your phone.
You may choose to cache your menu definition on your device in order to reduce the delay in showing the menu (as it saves waiting for an HTTP GET request). If you use this option you are responsible for managing the cache when the menu is updated at source. The toggle option below the cache option allows you to choose to refresh the cache the next time the application starts. Once the cache has been cleared, the application will reset this toggle for you, so you do not need to return to the settings to amend it.
You may choose to cache your menu definition on your device in order to reduce the delay in showing the menu (as it saves waiting for an HTTP GET request). If you use this option you need to be aware of hwo updates to the menu are managed. You may either:
The application uses vibration to confirm the action has been requested, as opposed to the 'toast' appears to show the action has been successfully executed. This is enabled by default but may be turned off if you do not desire this behaviour.
1. **Choose to have the cache cleared.** The toggle option below the cache option allows you to choose to refresh the cache the next time the application starts. Once the cache has been cleared, the application will reset this toggle for you, so you do not need to return to the settings to amend it.
2. **Let the application retrieve the menu after starting and setting up the switch states** (including evaluating [templates](examples/Templates.md)), and then verify you have the latest menu. If a newer menu is retrieved you will be notified via a 'toast' or blue screen for devices without a toast in their API. You will be prompted to restart the application in order to build the menu from this latest menu definition. There are no plans to make the menu definition update recreate the rendered menu items because it could change the selected item just as you action it, and because restarting is simple for the user and simpler for the code.
The application uses vibration to confirm the action has been requested, which is different to the 'toast' that appears to show the action has been successfully executed. This is enabled by default but may be turned off if you do not desire this behaviour.
The application timeout prevents the HomeAssistant App running on your watch when you have forgotten to close it. It prevents the refreshing of the menu statuses and therefore excessive wear on your battery level. For those users who prefer to keep the application open all the time for continuous use, they can reduce the battery wear by increasing the "poll delay". This inserts a user configurable number of seconds between each round of item update checks, hence reducing the API access activity. This also reduces the responsive of the statuses displayed when HA devices are switched externally, i.e. by another Home Assistant client, then the watch menu display will not update as quickly. Therefore if you only use the HomeAssistant App briefly now and then, keep this setting at the default 0 seconds. NB. To be clear, all items are updated then a configurable delay is inserted before the next round of all item updates. If your poll delay is greater than zero, then your application timeout should be set to zero, otherwise you will exit the application and negate the value of the poll delay function.
@@ -301,7 +304,7 @@ To prevent excessive battery usage, set the application timeout in the settings.
## 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. *Don't forget* you may need to choose to clear your cached menu.
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. *Don't forget* you may explicitly choose to clear your cached menu, or wait the application to discover your definition has changed and prompt you to restart. The application check only happens once after startup, rendering menu items and setting the menu item states.
## Submitting Corrections for Translations

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">مؤكد</string>
<string id="GlanceMenu" scope="glance">قائمة طعام</string>
<string id="Memory" scope="glance">ذاكرة</string>
<string id="MenuUpdated">محدثة القائمة ، إعادة التشغيل.</string>
<string id="NoAPIKey" scope="glance">لا مفتاح API في إعدادات التطبيق.</string>
<string id="NoApiUrl" scope="glance">لا عنوان URL API في إعدادات التطبيق.</string>
<string id="NoConfigUrl" scope="glance">لا يوجد عنوان URL للتكوين في إعدادات التطبيق.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Потвърдено</string>
<string id="GlanceMenu" scope="glance">Меню</string>
<string id="Memory" scope="glance">Памет</string>
<string id="MenuUpdated">Менюто актуализирано, рестартиране.</string>
<string id="NoAPIKey" scope="glance">Няма ключ за API в настройките на приложението.</string>
<string id="NoApiUrl" scope="glance">Няма URL адрес на API в настройките на приложението.</string>
<string id="NoConfigUrl" scope="glance">Няма URL адрес на конфигурация в настройките на приложението.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Potvrzeno</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Paměť</string>
<string id="MenuUpdated">Aktualizováno nabídka, restartujte.</string>
<string id="NoAPIKey" scope="glance">Žádný klíč API v nastavení aplikace.</string>
<string id="NoApiUrl" scope="glance">Žádná URL API v nastavení aplikace.</string>
<string id="NoConfigUrl" scope="glance">V nastavení aplikace není žádná konfigurační adresa URL.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Bekræftet</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Hukommelse</string>
<string id="MenuUpdated">Menu opdateret, genstart.</string>
<string id="NoAPIKey" scope="glance">Ingen API -nøgle i applikationsindstillingerne.</string>
<string id="NoApiUrl" scope="glance">Ingen API -URL i applikationsindstillingerne.</string>
<string id="NoConfigUrl" scope="glance">Ingen konfigurations -URL i applikationsindstillingerne.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Bestätigt</string>
<string id="GlanceMenu" scope="glance">Menü</string>
<string id="Memory" scope="glance">Speicher</string>
<string id="MenuUpdated">Menü aktualisiert, neu starten.</string>
<string id="NoAPIKey" scope="glance">Kein API-Schlüssel in den App-Einstellungen hinterlegt.</string>
<string id="NoApiUrl" scope="glance">Keine API-URL in den App-Einstellungen hinterlegt.</string>
<string id="NoConfigUrl" scope="glance">Keine Menükonfigurations-URL (JSON) in den App-Einstellungen hinterlegt.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Bevestigd</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Geheugen</string>
<string id="MenuUpdated">Menu bijgewerkt, opnieuw opstarten.</string>
<string id="NoAPIKey" scope="glance">Geen API -toets in de toepassingsinstellingen.</string>
<string id="NoApiUrl" scope="glance">Geen API -URL in de toepassingsinstellingen.</string>
<string id="NoConfigUrl" scope="glance">Geen configuratie -URL in de toepassingsinstellingen.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Kinnitatud</string>
<string id="GlanceMenu" scope="glance">Menüü</string>
<string id="Memory" scope="glance">Mälu</string>
<string id="MenuUpdated">Menüü värskendatud, taaskäivitage.</string>
<string id="NoAPIKey" scope="glance">Rakenduse seadetes pole API -klahvi.</string>
<string id="NoApiUrl" scope="glance">Rakenduse seadetes pole API URL -i.</string>
<string id="NoConfigUrl" scope="glance">Rakenduse sätetes pole konfiguratsiooni URL -i.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Vahvistettu</string>
<string id="GlanceMenu" scope="glance">Valikko</string>
<string id="Memory" scope="glance">Muisti</string>
<string id="MenuUpdated">Valikko päivitetty, käynnistä uudelleen.</string>
<string id="NoAPIKey" scope="glance">Ei sovellusliittymää avainta sovellusasetuksissa.</string>
<string id="NoApiUrl" scope="glance">Ei sovellus -URL -osoitetta sovellusasetuksissa.</string>
<string id="NoConfigUrl" scope="glance">Ei määritys -URL -osoitetta sovellusasetuksissa.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Confirmé</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Mémoire</string>
<string id="MenuUpdated">Menu mis à jour, redémarrer.</string>
<string id="NoAPIKey" scope="glance">Aucune clé API dans les paramètres de l'application.</string>
<string id="NoApiUrl" scope="glance">Aucune URL de l'API dans les paramètres de l'application.</string>
<string id="NoConfigUrl" scope="glance">Aucune URL de configuration dans les paramètres de l'application.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Επιβεβαιωμένος</string>
<string id="GlanceMenu" scope="glance">Μενού</string>
<string id="Memory" scope="glance">Μνήμη</string>
<string id="MenuUpdated">Ενημερώθηκε μενού, επανεκκίνηση.</string>
<string id="NoAPIKey" scope="glance">Δεν υπάρχει κλειδί API στις ρυθμίσεις εφαρμογής.</string>
<string id="NoApiUrl" scope="glance">Δεν υπάρχει διεύθυνση URL API στις ρυθμίσεις εφαρμογής.</string>
<string id="NoConfigUrl" scope="glance">Χωρίς διευθύνσεις διαμόρφωσης στις ρυθμίσεις εφαρμογής.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">מְאוּשָׁר</string>
<string id="GlanceMenu" scope="glance">תַפרִיט</string>
<string id="Memory" scope="glance">זֵכֶר</string>
<string id="MenuUpdated">מעודכן בתפריט, הפעל מחדש.</string>
<string id="NoAPIKey" scope="glance">אין מפתח API בהגדרות היישום.</string>
<string id="NoApiUrl" scope="glance">אין כתובת URL בהגדרות היישום.</string>
<string id="NoConfigUrl" scope="glance">אין כתובת אתר תצורה בהגדרות היישום.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Potvrđen</string>
<string id="GlanceMenu" scope="glance">Jelovnik</string>
<string id="Memory" scope="glance">Memorija</string>
<string id="MenuUpdated">Izbornik Ažurirano, ponovno pokrenite.</string>
<string id="NoAPIKey" scope="glance">Nema API ključa u postavkama aplikacije.</string>
<string id="NoApiUrl" scope="glance">Nema URL -a API -ja u postavkama aplikacije.</string>
<string id="NoConfigUrl" scope="glance">Nema URL -a konfiguracije u postavkama aplikacije.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Megerősített</string>
<string id="GlanceMenu" scope="glance">Menü</string>
<string id="Memory" scope="glance">Emlékezet</string>
<string id="MenuUpdated">A menü frissítve, indítsa újra.</string>
<string id="NoAPIKey" scope="glance">Nincs API -kulcs az alkalmazásbeállításokban.</string>
<string id="NoApiUrl" scope="glance">Nincs API URL az alkalmazás beállításaiban.</string>
<string id="NoConfigUrl" scope="glance">Nincs konfigurációs URL az alkalmazásbeállításokban.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Dikonfirmasi</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Ingatan</string>
<string id="MenuUpdated">Menu diperbarui, restart.</string>
<string id="NoAPIKey" scope="glance">Tidak ada kunci API di pengaturan aplikasi.</string>
<string id="NoApiUrl" scope="glance">Tidak ada URL API di pengaturan aplikasi.</string>
<string id="NoConfigUrl" scope="glance">Tidak ada URL konfigurasi di pengaturan aplikasi.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Confermato</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Memoria</string>
<string id="MenuUpdated">Menu aggiornato, riavvio.</string>
<string id="NoAPIKey" scope="glance">Nessuna chiave API nelle impostazioni dell'applicazione.</string>
<string id="NoApiUrl" scope="glance">Nessun URL API nelle impostazioni dell'applicazione.</string>
<string id="NoConfigUrl" scope="glance">Nessun URL di configurazione nelle impostazioni dell'applicazione.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">確認済み</string>
<string id="GlanceMenu" scope="glance">メニュー</string>
<string id="Memory" scope="glance">メモリ</string>
<string id="MenuUpdated">メニューの更新、再起動。</string>
<string id="NoAPIKey" scope="glance">アプリケーション設定にAPIキーはありません。</string>
<string id="NoApiUrl" scope="glance">アプリケーション設定にAPI URLはありません。</string>
<string id="NoConfigUrl" scope="glance">アプリケーション設定に構成URLはありません。</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">확인</string>
<string id="GlanceMenu" scope="glance">메뉴</string>
<string id="Memory" scope="glance">메모리</string>
<string id="MenuUpdated">메뉴가 업데이트되고 다시 시작됩니다.</string>
<string id="NoAPIKey" scope="glance">응용 프로그램 설정에 API 키가 없습니다.</string>
<string id="NoApiUrl" scope="glance">응용 프로그램 설정에 API URL이 없습니다.</string>
<string id="NoConfigUrl" scope="glance">응용 프로그램 설정에 구성 URL이 없습니다.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Apstiprināts</string>
<string id="GlanceMenu" scope="glance">Ēdienkarte</string>
<string id="Memory" scope="glance">Atmiņa</string>
<string id="MenuUpdated">Atjaunināta izvēlne, restartējiet.</string>
<string id="NoAPIKey" scope="glance">Lietojumprogrammas iestatījumos nav API atslēgas.</string>
<string id="NoApiUrl" scope="glance">Lietojumprogrammu iestatījumos nav API URL.</string>
<string id="NoConfigUrl" scope="glance">Lietojumprogrammas iestatījumos nav konfigurācijas URL.</string>
@@ -54,7 +55,7 @@
<string id="WifiLtePrompt">Izpildīt pār wi-fi/lte?</string>
<string id="WifiLteExecutionTitle">Sūtot mājas palīgu.</string>
<string id="WifiLteExecutionDataError">Dati nav saņemti.</string>
<!-- Iestatījumi GUI stīgām jābūt tādā secībā, kādā tās tiek izmantotas. -->
<!-- Iestatījumi GUI stīgām jābūt tādā secībā, kādu tie tiek izmantoti. -->
<string id="SettingsSelect">Atlasiet ...</string>
<string id="SettingsApiKey">API atslēga Homeassistant.</string>
<string id="SettingsApiKeyPrompt">Ilgstoša piekļuves marķieris.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Patvirtinta</string>
<string id="GlanceMenu" scope="glance">Meniu</string>
<string id="Memory" scope="glance">Atmintis</string>
<string id="MenuUpdated">Meniu Atnaujinta, paleiskite iš naujo.</string>
<string id="NoAPIKey" scope="glance">Nėra API rakto programos nustatymuose.</string>
<string id="NoApiUrl" scope="glance">Nėra API URL programos nustatymuose.</string>
<string id="NoConfigUrl" scope="glance">Nėra konfigūracijos URL programos nustatymuose.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Bekreftet</string>
<string id="GlanceMenu" scope="glance">Meny</string>
<string id="Memory" scope="glance">Hukommelse</string>
<string id="MenuUpdated">Meny oppdatert, omstart.</string>
<string id="NoAPIKey" scope="glance">Ingen API -nøkkel i applikasjonsinnstillingene.</string>
<string id="NoApiUrl" scope="glance">Ingen API -URL i applikasjonsinnstillingene.</string>
<string id="NoConfigUrl" scope="glance">Ingen konfigurasjons -URL i applikasjonsinnstillingene.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Potwierdzony</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Pamięć</string>
<string id="MenuUpdated">Zaktualizowano menu, uruchom ponownie.</string>
<string id="NoAPIKey" scope="glance">Brak klucza API w ustawieniach aplikacji.</string>
<string id="NoApiUrl" scope="glance">Brak adresu URL API w ustawieniach aplikacji.</string>
<string id="NoConfigUrl" scope="glance">Brak adresu URL konfiguracji w ustawieniach aplikacji.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Confirmado</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Memória</string>
<string id="MenuUpdated">Menu Atualizado, reinicie.</string>
<string id="NoAPIKey" scope="glance">Nenhuma chave da API nas configurações do aplicativo.</string>
<string id="NoApiUrl" scope="glance">Nenhum URL da API nas configurações do aplicativo.</string>
<string id="NoConfigUrl" scope="glance">Nenhum URL de configuração nas configurações do aplicativo.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Confirmat</string>
<string id="GlanceMenu" scope="glance">Meniu</string>
<string id="Memory" scope="glance">Memorie</string>
<string id="MenuUpdated">Meniu actualizat, reporniți.</string>
<string id="NoAPIKey" scope="glance">Nici o cheie API în setările aplicației.</string>
<string id="NoApiUrl" scope="glance">Fără URL API în setările aplicației.</string>
<string id="NoConfigUrl" scope="glance">Fără URL de configurare în setările aplicației.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Spustený</string>
<string id="GlanceMenu" scope="glance">Ponuka</string>
<string id="Memory" scope="glance">Pamiatka</string>
<string id="MenuUpdated">Aktualizované menu, reštart.</string>
<string id="NoAPIKey" scope="glance">V nastaveniach aplikácie chýba kľúč API</string>
<string id="NoApiUrl" scope="glance">V nastaveniach aplikácie chýba adresa URL rozhrania API</string>
<string id="NoConfigUrl" scope="glance">V nastaveniach aplikácie chýba konfiguračná adresa URL</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Potrjeno</string>
<string id="GlanceMenu" scope="glance">Meni</string>
<string id="Memory" scope="glance">Spomin</string>
<string id="MenuUpdated">Meni posodobljen, znova zaženite.</string>
<string id="NoAPIKey" scope="glance">V nastavitvah aplikacije ni ključa API -ja.</string>
<string id="NoApiUrl" scope="glance">V nastavitvah aplikacije ni URL API -ja.</string>
<string id="NoConfigUrl" scope="glance">V nastavitvah aplikacije ni URL konfiguracije.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Confirmado</string>
<string id="GlanceMenu" scope="glance">Menú</string>
<string id="Memory" scope="glance">Memoria</string>
<string id="MenuUpdated">Menú actualizado, reiniciar.</string>
<string id="NoAPIKey" scope="glance">No hay clave API en la configuración de la aplicación.</string>
<string id="NoApiUrl" scope="glance">No hay URL de API en la configuración de la aplicación.</string>
<string id="NoConfigUrl" scope="glance">No hay URL de configuración en la configuración de la aplicación.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Bekräftad</string>
<string id="GlanceMenu" scope="glance">Meny</string>
<string id="Memory" scope="glance">Minne</string>
<string id="MenuUpdated">Meny Uppdaterad, omstart.</string>
<string id="NoAPIKey" scope="glance">Ingen API -nyckel i applikationsinställningarna.</string>
<string id="NoApiUrl" scope="glance">Ingen API -URL i applikationsinställningarna.</string>
<string id="NoConfigUrl" scope="glance">Ingen konfigurations -URL i applikationsinställningarna.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">ที่ได้ยืนยันแล้ว</string>
<string id="GlanceMenu" scope="glance">เมนู</string>
<string id="Memory" scope="glance">หน่วยความจำ</string>
<string id="MenuUpdated">อัปเดตเมนูรีสตาร์ท</string>
<string id="NoAPIKey" scope="glance">ไม่มีคีย์ API ในการตั้งค่าแอปพลิเคชัน</string>
<string id="NoApiUrl" scope="glance">ไม่มี URL API ในการตั้งค่าแอปพลิเคชัน</string>
<string id="NoConfigUrl" scope="glance">ไม่มี URL การกำหนดค่าในการตั้งค่าแอปพลิเคชัน</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Onaylanmış</string>
<string id="GlanceMenu" scope="glance">Menü</string>
<string id="Memory" scope="glance">Hafıza</string>
<string id="MenuUpdated">Menü güncellendi, yeniden başlat.</string>
<string id="NoAPIKey" scope="glance">Uygulama ayarlarında API anahtarı yok.</string>
<string id="NoApiUrl" scope="glance">Uygulama ayarlarında API URL'si yok.</string>
<string id="NoConfigUrl" scope="glance">Uygulama ayarlarında yapılandırma URL'si yok.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Підтверджений</string>
<string id="GlanceMenu" scope="glance">Меню</string>
<string id="Memory" scope="glance">Пам'ять</string>
<string id="MenuUpdated">Меню оновлено, перезапустіть.</string>
<string id="NoAPIKey" scope="glance">Немає ключа API в налаштуваннях програми.</string>
<string id="NoApiUrl" scope="glance">Немає URL -адреси API в налаштуваннях програми.</string>
<string id="NoConfigUrl" scope="glance">Немає URL -адреси конфігурації в налаштуваннях програми.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Xác nhận</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Ký ức</string>
<string id="MenuUpdated">Menu được cập nhật, khởi động lại.</string>
<string id="NoAPIKey" scope="glance">Không có khóa API trong cài đặt ứng dụng.</string>
<string id="NoApiUrl" scope="glance">Không có URL API trong cài đặt ứng dụng.</string>
<string id="NoConfigUrl" scope="glance">Không có URL cấu hình trong cài đặt ứng dụng.</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">确认的</string>
<string id="GlanceMenu" scope="glance">菜单</string>
<string id="Memory" scope="glance">记忆</string>
<string id="MenuUpdated">菜单更新,重新启动。</string>
<string id="NoAPIKey" scope="glance">应用程序设置中没有API密钥。</string>
<string id="NoApiUrl" scope="glance">应用程序设置中没有API URL。</string>
<string id="NoConfigUrl" scope="glance">应用程序设置中没有配置URL。</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">確認的</string>
<string id="GlanceMenu" scope="glance">菜單</string>
<string id="Memory" scope="glance">記憶</string>
<string id="MenuUpdated">菜單更新,重新啟動。</string>
<string id="NoAPIKey" scope="glance">應用程序設置中沒有API密鑰。</string>
<string id="NoApiUrl" scope="glance">應用程序設置中沒有API URL。</string>
<string id="NoConfigUrl" scope="glance">應用程序設置中沒有配置URL。</string>

View File

@@ -31,6 +31,7 @@
<string id="Executed" scope="glance">Disahkan</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Ingatan</string>
<string id="MenuUpdated">Menu dikemas kini, mulakan semula.</string>
<string id="NoAPIKey" scope="glance">Tiada kunci API dalam tetapan aplikasi.</string>
<string id="NoApiUrl" scope="glance">Tiada URL API dalam tetapan aplikasi.</string>
<string id="NoConfigUrl" scope="glance">Tiada URL Konfigurasi dalam Tetapan Aplikasi.</string>

View File

@@ -25,6 +25,7 @@
<string id="Executed" scope="glance">Confirmed</string>
<string id="GlanceMenu" scope="glance">Menu</string>
<string id="Memory" scope="glance">Memory</string>
<string id="MenuUpdated">Menu updated, restart.</string>
<string id="NoAPIKey" scope="glance">No API key in the application settings.</string>
<string id="NoApiUrl" scope="glance">No API URL in the application settings.</string>
<string id="NoConfigUrl" scope="glance">No configuration URL in the application settings.</string>

View File

@@ -16,6 +16,8 @@
using Toybox.Application;
using Toybox.Communications;
using Toybox.Lang;
// Required for callback method definition
typedef Method as Toybox.Lang.Method;
using Toybox.WatchUi;
using Toybox.System;
using Toybox.Application.Properties;
@@ -40,6 +42,7 @@ class HomeAssistantApp extends Application.AppBase {
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
private var mTemplates as Lang.Dictionary = {};
private var mNotifiedNoBle as Lang.Boolean = false;
private var mIsCacheChecked as Lang.Boolean = false;
//! Class Constructor
//
@@ -257,39 +260,14 @@ class HomeAssistantApp extends Application.AppBase {
} else {
var menu = Storage.getValue("menu") as Lang.Dictionary;
if (menu != null and (Settings.getClearCache() || !Settings.getCacheConfig())) {
// System.println("HomeAssistantApp fetchMenuConfig(): Clearing cached menu on user request.");
Storage.deleteValue("menu");
menu = null;
Settings.unsetClearCache();
}
if (menu == null) {
var phoneConnected = System.getDeviceSettings().phoneConnected;
var internetAvailable = System.getDeviceSettings().connectionAvailable;
if (! phoneConnected or ! internetAvailable) {
// System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call.");
var errorRez = $.Rez.Strings.NoPhone;
if (Settings.getWifiLteExecutionEnabled()) {
errorRez = $.Rez.Strings.NoPhoneNoCache;
} else if (! internetAvailable) {
errorRez = $.Rez.Strings.Unavailable;
}
if (!mIsApp) {
WatchUi.requestUpdate();
} else {
ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
}
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
} else {
Communications.makeWebRequest(
Settings.getConfigUrl(),
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
:headers => Settings.augmentHttpHeaders({})
},
method(:onReturnFetchMenuConfig)
);
}
// System.println("HomeAssistantApp fetchMenuConfig(): Menu not cached, fetching.");
fetchMenuConfigBasic(method(:onReturnFetchMenuConfig));
} else {
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
WatchUi.requestUpdate();
@@ -304,6 +282,56 @@ class HomeAssistantApp extends Application.AppBase {
return false;
}
//! The basic API call to fetch the menu configuration, with cache management issues managed by external supporting
//! code. This is factored out separately so that it can be reused to check and validate the cached menu.
//!
//! @param responseCallback The method to call on completion of the GET request.
//
function fetchMenuConfigBasic(
responseCallback as (
Method(
responseCode as Lang.Number,
data as Lang.Dictionary or Lang.String or Null
) as Void
) or (
Method(
responseCode as Lang.Number,
data as Lang.Dictionary or Lang.String or Null,
context as Lang.Object
) as Void
)
) {
// System.println("HomeAssistantApp fetchMenuConfigBasic(): Fetching JSON menu.");
var phoneConnected = System.getDeviceSettings().phoneConnected;
var internetAvailable = System.getDeviceSettings().connectionAvailable;
if (! phoneConnected or ! internetAvailable) {
// System.println("HomeAssistantApp fetchMenuConfigBasic(): No Phone connection, skipping API call.");
var errorRez = $.Rez.Strings.NoPhone;
if (Settings.getWifiLteExecutionEnabled()) {
errorRez = $.Rez.Strings.NoPhoneNoCache;
} else if (! internetAvailable) {
errorRez = $.Rez.Strings.Unavailable;
}
if (!mIsApp) {
WatchUi.requestUpdate();
} else {
ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
}
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
} else {
Communications.makeWebRequest(
Settings.getConfigUrl(),
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
:headers => Settings.augmentHttpHeaders({})
},
responseCallback
);
}
}
//! Build the menu and store in `mHaMenu`. Then start updates if necessary.
//!
//! @param menu The dictionary derived from the JSON menu fetched by `fetchMenuConfig()`.
@@ -342,6 +370,173 @@ class HomeAssistantApp extends Application.AppBase {
}
}
//! Test if two dictionaries are structurally equal. Used to see if the JSON menu has been
//! amended but yet to be updated in the application cache.
//!
//! @param a First dictionary in the comparison.
//! @param b Second dictionary in the comparison.
//
function structuralEquals(
a as Lang.Dictionary,
b as Lang.Dictionary
) as Lang.Boolean {
if (a.size() != b.size()) {
return false;
}
var keys = a.keys();
for (var i = 0; i < keys.size(); i++) {
var key = keys[i];
// If the sizes are the same and b contains every item in a,
// then a contains every item in b, i.e. the items are the same.
if (!b.hasKey(key)) {
return false;
}
var valA = a.get(key);
var valB = b.get(key);
if (valA == null && valB == null) {
// both null, consider true
} else if (valA == null || valB == null) {
return false;
} else if (valA instanceof Lang.Dictionary and valB instanceof Lang.Dictionary) {
if (!structuralEquals(valA, valB)) {
return false;
}
} else if (valA instanceof Lang.Array and valB instanceof Lang.Array) {
if (!arrayEquals(valA, valB)) {
return false;
}
} else if (!valA.equals(valB)) {
return false;
}
}
return true;
}
//! Test if two arrays are structurally equal. Used to see if the JSON menu has been
//! amended but yet to be updated in the application cache.
//!
//! @param a First array in the comparison.
//! @param b Second array in the comparison.
//
function arrayEquals(
a as Lang.Array,
b as Lang.Array
) as Lang.Boolean {
if (a.size() != b.size()) {
return false;
}
for (var j = 0; j < a.size(); j++) {
var itemA = a[j];
var itemB = b[j];
if (itemA == null && itemB == null) {
// both null, consider true
} else if (itemA == null || itemB == null) {
return false;
} else if (itemA instanceof Lang.Dictionary and itemB instanceof Lang.Dictionary) {
if (!structuralEquals(itemA, itemB)) {
return false;
}
} else if (valA instanceof Lang.Array and valB instanceof Lang.Array) {
if (!arrayEquals(valA, valB)) {
return false;
}
} else if (!itemA.equals(itemB)) {
return false;
}
}
}
//! Callback function for the menu check GET request.
//!
//! @param responseCode Response code.
//! @param data Response data.
//
function onReturnCheckMenuConfig(
responseCode as Lang.Number,
data as Null or Lang.Dictionary
) as Void {
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Data: " + data);
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() 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(method(:updateMenuItems), Globals.scApiBackoffMs, false);
break;
case 404:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
case 400:
// System.println("HomeAssistantApp onReturnCheckMenuConfig() Response Code: 400, bad request. Template error.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
break;
case 200:
if (data != null) {
// 'menu' will be null if caching has just been enabled, but not yet cached locally.
var menu = Storage.getValue("menu") as Lang.Dictionary;
if (menu == null || !structuralEquals(data, menu)) {
// System.println("HomeAssistantApp onReturnCheckMenuConfig() New menu found.");
Storage.setValue("menu", data as Lang.Dictionary);
if (menu != null) {
// Notify the the user we have just got a newer menu file
var toast = WatchUi.loadResource($.Rez.Strings.MenuUpdated) as Lang.String;
if (mHasToast) {
WatchUi.showToast(toast, null);
} else {
new Alert({
:timeout => Globals.scAlertTimeoutMs,
:font => Graphics.FONT_MEDIUM,
:text => toast,
:fgcolor => Graphics.COLOR_WHITE,
:bgcolor => Graphics.COLOR_BLACK
}).pushView(WatchUi.SLIDE_IMMEDIATE);
}
}
}
// Prevent checking the cache is up to date again
mIsCacheChecked = true;
var delay = Settings.getPollDelay();
if (delay > 0) {
mUpdateTimer.start(method(:updateMenuItems), delay, false);
} else {
updateMenuItems();
}
}
break;
default:
// System.println("HomeAssistantApp onReturnCheckMenuConfig(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
}
//! Callback function for each menu update GET request.
//!
//! @param responseCode Response code.
@@ -405,18 +600,23 @@ class HomeAssistantApp extends Application.AppBase {
} else {
if (mItemsToUpdate != null) {
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var item = mItemsToUpdate[i];
var state = data.get(i.toString());
item.updateState(state);
if (item instanceof HomeAssistantToggleMenuItem) {
(item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t"));
}
}
var delay = Settings.getPollDelay();
if (delay > 0) {
mUpdateTimer.start(method(:updateMenuItems), delay, false);
if (Settings.getCacheConfig() && !mIsCacheChecked) {
// We are caching the menu configuration, so let's fetch it and check if its been updated.
fetchMenuConfigBasic(method(:onReturnCheckMenuConfig));
} else {
updateMenuItems();
var delay = Settings.getPollDelay();
if (delay > 0) {
mUpdateTimer.start(method(:updateMenuItems), delay, false);
} else {
updateMenuItems();
}
}
}
}
@@ -605,7 +805,6 @@ class HomeAssistantApp extends Application.AppBase {
if (!mIsApp) {
WatchUi.requestUpdate();
} else {
System.println("we here");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
}
} else if (! connectionAvailable) {

View File

@@ -14,8 +14,6 @@
//-----------------------------------------------------------------------------------
using Toybox.Lang;
// Required for callback method definition
typedef Method as Toybox.Lang.Method;
using Toybox.WatchUi;
using Toybox.Timer;
using Toybox.Application.Properties;

View File

@@ -128,18 +128,18 @@ class HomeAssistantService {
data as Lang.Dictionary?,
exit as Lang.Boolean
) as Void {
var phoneConnected = System.getDeviceSettings().phoneConnected;
var phoneConnected = System.getDeviceSettings().phoneConnected;
var internetAvailable = System.getDeviceSettings().connectionAvailable;
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! internetAvailable)) {
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
var dialog = new WatchUi.Confirmation(dialogMsg);
var dialog = new WatchUi.Confirmation(dialogMsg);
WatchUi.pushView(
dialog,
new WifiLteExecutionConfirmDelegate({
:type => "service",
:type => "service",
:service => service,
:data => data,
:exit => exit,
:data => data,
:exit => exit,
}, dialog),
WatchUi.SLIDE_LEFT
);