Compare commits

...

5 Commits

Author SHA1 Message Date
Philip Abbey
3d7b588d2c small doc suggestions (#271)
Thank you!
2025-08-10 20:52:29 +01:00
Philip Abbey
166b5f4ec3 Found a path to a crash condition
Preventing a de-reference of null when the HA server is unreachable.
2025-08-10 20:50:12 +01:00
Philip Abbey
57128bf7a4 Update README.md
Amended path to create security tokens.
2025-08-10 19:09:52 +01:00
Philip Abbey
64bebded0a Update GarminHomeAssistantSettings.png 2025-08-10 18:42:48 +01:00
tispokes
b9db9af3bf small doc suggestions
small doc suggestions

small doc suggestions
2025-08-10 18:42:28 +02:00
5 changed files with 84 additions and 72 deletions

View File

@@ -4,6 +4,8 @@
"Venu" "Venu"
], ],
"files.exclude": { "files.exclude": {
"resources-*": true "resources-*": true,
"bin": true,
"export": true
} }
} }

View File

@@ -2,7 +2,7 @@
# User Specified Custom HTTP Headers # User Specified Custom HTTP Headers
Principally for those who use Home Assistant add-on [Cloudflared](https://github.com/brenner-tobias/addon-cloudflared) in order to provide additional security via Cloudflare's Web Application Firewall (WAF). But the solution is generic enough for other use cases. Principally for those who use Home Assistant add-on [Cloudflared](https://github.com/brenner-tobias/addon-cloudflared) in order to provide additional security via Cloudflare's Web Application Firewall (WAF). But Garmin does not support certificates in requests. And the solution is generic enough for other use cases.
Please let us know if this solution is found to be useful for other situations. Please let us know if this solution is found to be useful for other situations.
@@ -14,6 +14,12 @@ The settings contain two options for users to specify both the HTTP header name
If you don't know why you need these, leave them empty and ignore. If you don't know why you need these, leave them empty and ignore.
### Cloudflare WAF rule example
`(any(http.request.headers["your-header-name"][*] eq "your-header-key"))`
Make the key strong enough!
## Support ## Support
**None!** **None!**

View File

@@ -17,6 +17,8 @@ The application is designed around a simple scrollable menu where menu items hav
> [!IMPORTANT] > [!IMPORTANT]
> The Garmin SDK allows HTTP requests only to a limited number of domains specified in their app. Therefore, for your Garmin to communicate with your Home Assistant instance, your Home Assistant instance must be accessible via HTTPS (with a public certificate!) or through a local DNS server that overrides one of the whitelisted domains to communicate using HTTP. > The Garmin SDK allows HTTP requests only to a limited number of domains specified in their app. Therefore, for your Garmin to communicate with your Home Assistant instance, your Home Assistant instance must be accessible via HTTPS (with a public certificate!) or through a local DNS server that overrides one of the whitelisted domains to communicate using HTTP.
> >
>New with version 3.1, you can use [Cloudflared](https://github.com/brenner-tobias/addon-cloudflared) plug-in in combination with a [custom HTTP header](HTTP_Headers.md) and do not need a public certificate for HTTPS.
>
> To make your Home Assistant instance accessible via HTTPS, you will need 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/). (You can install a local [Nginx proxy server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_nginxproxymanager) to manage Let's Encrypt certificates.) > To make your Home Assistant instance accessible via HTTPS, you will need 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/). (You can install a local [Nginx proxy server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_nginxproxymanager) to manage Let's Encrypt certificates.)
> >
> If you use a local DNS server (like [Pi-Hole](https://pi-hole.net/)), you can create a local DNS record for the domain `garmincdn.com` (which is allowed for HTTP in the Garmin SDK) and map it to your Home Assistant instance's IP. "_[About Communication Between Garmin SDK and a Raspberry Pi](https://www.instructables.com/About-Communication-Between-Garmin-SDK-and-a-Raspb/)_" provides additional workarounds for HTTP request restrictions in the Garmin SDK. > If you use a local DNS server (like [Pi-Hole](https://pi-hole.net/)), you can create a local DNS record for the domain `garmincdn.com` (which is allowed for HTTP in the Garmin SDK) and map it to your Home Assistant instance's IP. "_[About Communication Between Garmin SDK and a Raspberry Pi](https://www.instructables.com/About-Communication-Between-Garmin-SDK-and-a-Raspb/)_" provides additional workarounds for HTTP request restrictions in the Garmin SDK.
@@ -230,7 +232,7 @@ Make sure you can browse to the URL of your JSON file in a standard web browser
## API Key Creation ## API Key Creation
Having created your JSON definition for your dashboard, you need to create an API key for your personal account on Home Assistant. You will need a [Long-Lived Access Token](https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token). This is not obvious to find and is bound to your own Home Assistant account. Follow the menu sequence: `HA -> user profile -> Long-lived access tokens`. Make sure you save the generated token before dismissing it. Having created your JSON definition for your dashboard, you need to create an API key for your personal account on Home Assistant. You will need a [Long-Lived Access Token](https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token). This is not obvious to find and is bound to your own Home Assistant account. Follow the menu sequence: `HA -> User Profile -> "Security" tab -> Long-lived access tokens`. Make sure you save the generated token before dismissing it.
![Long-Lived Access Token](images/Long_Lived_Access_Tokens.png) ![Long-Lived Access Token](images/Long_Lived_Access_Tokens.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 KiB

After

Width:  |  Height:  |  Size: 557 KiB

View File

@@ -430,85 +430,87 @@ class HomeAssistantApp extends Application.AppBase {
//! Construct the GET request to update all menu items. //! Construct the GET request to update all menu items.
// //
function updateMenuItems() as Void { function updateMenuItems() as Void {
var phoneConnected = System.getDeviceSettings().phoneConnected; if (mUpdating) {
var connectionAvailable = System.getDeviceSettings().connectionAvailable; var phoneConnected = System.getDeviceSettings().phoneConnected;
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
// In Wi-Fi/LTE execution mode, we should not show an error page but use a toast instead. // In Wi-Fi/LTE execution mode, we should not show an error page but use a toast instead.
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) { if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
// Notify only once per disconnection cycle // Notify only once per disconnection cycle
if (!mNotifiedNoBle) { if (!mNotifiedNoBle) {
var toast = WatchUi.loadResource($.Rez.Strings.NoPhone); var toast = WatchUi.loadResource($.Rez.Strings.NoPhone);
if (!connectionAvailable) { if (!connectionAvailable) {
toast = WatchUi.loadResource($.Rez.Strings.NoInternet); toast = WatchUi.loadResource($.Rez.Strings.NoInternet);
}
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);
}
} }
if (mHasToast) { mNotifiedNoBle = true;
WatchUi.showToast(toast, null); setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else { mUpdateTimer.start(method(:startUpdates), Globals.wifiPollResumeDelayMs, false);
new Alert({
:timeout => Globals.scAlertTimeoutMs, mUpdating = false;
:font => Graphics.FONT_MEDIUM, return;
:text => toast,
:fgcolor => Graphics.COLOR_WHITE,
:bgcolor => Graphics.COLOR_BLACK
}).pushView(WatchUi.SLIDE_IMMEDIATE);
}
} }
mNotifiedNoBle = true; if (! phoneConnected) {
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); // System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
mUpdateTimer.start(method(:startUpdates), Globals.wifiPollResumeDelayMs, false); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! connectionAvailable) {
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else {
mNotifiedNoBle = false;
mUpdating = false; if (mItemsToUpdate == null or mTemplates == null) {
return; mItemsToUpdate = mHaMenu.getItemsToUpdate();
} mTemplates = {};
for (var i = 0; i < mItemsToUpdate.size(); i++) {
if (! phoneConnected) { var item = mItemsToUpdate[i];
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call."); var template = item.getTemplate();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String); if (template != null) {
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); mTemplates.put(i.toString(), {
} else if (! connectionAvailable) { "template" => template
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call."); });
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); }
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); if (item instanceof HomeAssistantToggleMenuItem) {
} else { mTemplates.put(i.toString() + "t", {
mNotifiedNoBle = false; "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate()
if (mItemsToUpdate == null or mTemplates == null) {
mItemsToUpdate = mHaMenu.getItemsToUpdate();
mTemplates = {};
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var template = item.getTemplate();
if (template != null) {
mTemplates.put(i.toString(), {
"template" => template
}); });
} }
if (item instanceof HomeAssistantToggleMenuItem) { }
mTemplates.put(i.toString() + "t", {
"template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate()
});
}
} }
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
{
"type" => "render_template",
"data" => mTemplates
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => Settings.augmentHttpHeaders({
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
}),
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnUpdateMenuItems)
);
} }
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
{
"type" => "render_template",
"data" => mTemplates
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => Settings.augmentHttpHeaders({
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
}),
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnUpdateMenuItems)
);
} }
} }