Compare commits
42 Commits
v2.22
...
CI-Release
Author | SHA1 | Date | |
---|---|---|---|
cb819fce73 | |||
ef299bcaf6 | |||
f0eb9c26b1 | |||
f1c592179d | |||
4ed81df60a | |||
b4f5f34760 | |||
4cf5a0ae26 | |||
b44d4c6155 | |||
f2d65aa6e3 | |||
6b2aa3135a | |||
907848b5fb | |||
5f34f870d9 | |||
ada18f8323 | |||
f74a3168de | |||
7e58e5416d | |||
1f7090092f | |||
4ed132b9ca | |||
fd213cc210 | |||
7423e609d8 | |||
9df43cdb01 | |||
5ee8a6cc6a | |||
5e35276628 | |||
28f4ffc70b | |||
e8f2c0d3bb | |||
71cba8c21c | |||
236a4969d7 | |||
e41f451fbc | |||
c13e898e59 | |||
a77870c2eb | |||
155649f162 | |||
1ae4085113 | |||
7a14a65ba3 | |||
adf69d7dec | |||
d105e69484 | |||
184e2ac80e | |||
12c5766818 | |||
b82e63d191 | |||
ed4646511f | |||
fd58625640 | |||
816f2e6399 | |||
f171f913ac | |||
cca73a4069 |
32
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Release
|
||||
run-name: ${{ github.actor }} is releasing 🚀
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
Create-Release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Decrypt developer key
|
||||
# see https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#storing-large-secrets
|
||||
run: ./.github/scripts/decrypt_secret.sh
|
||||
env:
|
||||
INPUT: ./developer_key.gpg
|
||||
OUTPUT: ./developer_key
|
||||
PASSPHRASE: ${{ secrets.DEVELOPER_KEY_PASSPHRASE }}
|
||||
- name: Create release
|
||||
uses: blackshadev/garmin-connectiq-release-action@8.2.1
|
||||
with:
|
||||
projectJungle: ./monkey.jungle
|
||||
developerKey: ./developer_key
|
||||
outputPath: out/app.iq
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: garmin-home-assistant-${{ github.ref_name }}
|
||||
path: out/app.iq
|
@ -35,3 +35,10 @@
|
||||
| 2.20 | Simplified the code base now that templates have been requested in all menu items. This means the `template` menu item became a superset of `tap`. Therefore the `tap` code has been has been upgraded to include `template` and the latter deprecated. JSON menu definitions continue to support `template` items by instantiating a `tap` menu item, but the schema marks them as deprecated and users should migrate their menu definitions now. Use the [web editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) for assistance with changes. |
|
||||
| 2.21 | Added 7 new devices (`edge1050`, `enduro3`, `fenix843mm`, `fenix847mm`, `fenix8solar47mm`, `fenix8solar51mm`, `fenixe`) and upgraded the SDK to 7.3.0. Fix for a bug on Edge devices introduced by v2.16 activity reporting improvements. |
|
||||
| 2.22 | Major feature release adding an optional PIN to menu items. This significant new feature has been provided by [moesterheld](https://github.com/moesterheld). Please do not rely on this application for security. Use at your own risk! |
|
||||
| 2.23 | Added "info" menu item for displaying information via a template without a tap or toggle. Essentially like the old 'template' type that was deprecated when all items were amended to display evaluated templates. That action removed the display only items too hastily. Added 5 new devices in the model range Instinct 3 and Instinct E. |
|
||||
| 2.24 | Experiment to prevent new Webhook IDs being created unnecessarily. Reduced the latency for the first menu update. Added 4 new devices: approachs50, descentg2, descentmk1, and gpsmap66. |
|
||||
| 2.25 | 2 Bug fixes. First time startup issues caused by v2.24 change and a fix for pure numbers in returned templates. |
|
||||
| 2.26 | Retry responsive menu fix failed in v2.24. Cosmetic internal class changes. |
|
||||
| 2.27 | Trivial bug fix for the glance view to prevent the "Unconfigured" result being erroneously displayed because the settings were not yet pulled from persistent storage. |
|
||||
| 2.28 | Added support for Vivoactive 6 device which also required an SDK update to 8.1.0. |
|
||||
| 2.29 | Added support for three new devices, Forerunners 570 42mm & 47mm and 970. |
|
||||
|
@ -4,5 +4,9 @@
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
"settings": {
|
||||
"cSpell.words": [
|
||||
"Initialiser"
|
||||
]
|
||||
}
|
||||
}
|
61
README.md
@ -6,12 +6,14 @@ Home | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Temp
|
||||
|
||||
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, or a text status for an entity (template 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, or a text status for an entity (`info` 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 intended audience for this application are those comfortable with configuring a Home Assistant** (e.g. editing the YAML configuration files) and debugging why URLs don't work. It does not require programming skills, but the menu is configured via JSON which feels like "coding". If you are not comfortable with this relatively low level of configuration, you may like to try other Garmin applications instead.
|
||||
**The intended audience for this application are those comfortable with configuring a Home Assistant** (e.g. editing the YAML configuration files) and debugging why URLs don't work. It does not require programming skills, but the menu is configured via JSON which feels like "coding" (more like "describing"). If you are not comfortable with this relatively low level of configuration, you may like to try other Garmin applications instead.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 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/). (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.)
|
||||
> 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.
|
||||
> 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. You can find additional workarounds for HTTP request restrictions in the Garmin SDK [here](https://www.instructables.com/About-Communication-Between-Garmin-SDK-and-a-Raspb/).
|
||||
|
||||
**If you are struggling with getting the application to work, please consult the [trouble shooting](TroubleShooting.md#menu-configuration-url) guide first.**
|
||||
|
||||
@ -143,32 +145,33 @@ NB. Entity names are not real in case anyone's a hacker ;-).
|
||||
|
||||
The example above illustrates how to configure:
|
||||
|
||||
* Lights or switches (toggle), <img src="images/toggle_icon.png" height="20">
|
||||
* Enables for automations (toggle), <img src="images/toggle_icon.png" height="20">
|
||||
* Script invocation (tap)
|
||||
* Service invocation, e.g. Scene setting, (tap)
|
||||
* A sub-menu to open (group)
|
||||
* You can also display the status of devices (template) and add an optional 'tap' action. However that's a bit more involved and has its own [examples page](examples/Templates.md). Add those later!
|
||||
* Lights or switches (`toggle`), <img src="images/toggle_icon.png" height="20">
|
||||
* Enables for automations (`toggle`), <img src="images/toggle_icon.png" height="20">
|
||||
* Script invocation (`tap`)
|
||||
* Service invocation, e.g. Scene setting, (`tap`)
|
||||
* A sub-menu to open (`group`)
|
||||
* You can also display the status of devices (`info`) which is essentially a `tap` with no action
|
||||
* All menu items can display the results of evaluating [templates](examples/Templates.md).
|
||||
|
||||
The following table indicates how Home Assistant entity types can map to the Garmin applications menu types. Presently, an automation is the only one that can be either a 'tap' or a 'toggle'.
|
||||
The following table indicates how Home Assistant entity types can map to the Garmin applications menu types. Presently, an automation is the only one that can be either a `tap` or a `toggle`.
|
||||
|
||||
| HA Entity Type | Tap | Toggle | Template (custom status text with optional tap action) |
|
||||
|------------------|:---:|:------:|:------------------------------------------------------:|
|
||||
| Switch | ❌ | ✅ | ✅<br>Separate on and off, or anything in between |
|
||||
| Light | ❌ | ✅ | ✅<br>Separate on and off, or anything in between |
|
||||
| Automation | ✅ | ✅ | ✅ |
|
||||
| Script | ✅ | ❌ | ✅ |
|
||||
| Scene | ✅ | ❌ | ✅ |
|
||||
| Sensor | ❌ | ❌ | ✅ |
|
||||
| Binary Sensor | ❌ | ❌ | ✅ |
|
||||
| Any other entity | ❌ | ❌ | ✅ |
|
||||
| Any service | ✅ | ❌ | ✅ |
|
||||
| HA Entity Type | Tap | Toggle | Info (status)|
|
||||
|------------------|:---:|:------:|:------------:|
|
||||
| Switch | ❌ | ✅ | ✅ |
|
||||
| Light | ❌ | ✅ | ✅ |
|
||||
| Automation | ✅ | ✅ | ❌ |
|
||||
| Script | ✅ | ❌ | ❌ |
|
||||
| Scene | ✅ | ❌ | ❌ |
|
||||
| Sensor | ❌ | ❌ | ✅ |
|
||||
| Binary Sensor | ❌ | ❌ | ✅ |
|
||||
| Any other entity | ❌ | ❌ | ✅ |
|
||||
| Any service | ✅ | ❌ | ❌ |
|
||||
|
||||
Templates need separate HTTP requests to update their status and send an action. Only the toggle items have the on/off <img src="images/toggle_icon.png" height="20"> icon. A Tap does not require a status update and hence does not require the associated HTTP GET request. NB. All 'tap' items must specify a 'service' tag.
|
||||
Multiple templates are evaluated in a single HTTP request to update their status. Only the toggle items have the on/off <img src="images/toggle_icon.png" height="20"> icon. NB. All `tap` items must specify a `service` tag in the `tap_action` object (see example below).
|
||||
|
||||
You can now specify alternative texts to use instead of "On" and "Off", e.g. "Locked" and "Unlocked" or "Open" and "Closed" through the use of a [template menu item](examples/Templates.md). 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 to protect itself.
|
||||
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 to protect itself.
|
||||
|
||||
### Old deprecated format
|
||||
|
||||
@ -196,13 +199,13 @@ The above should be replaced by the following:
|
||||
}
|
||||
```
|
||||
|
||||
This allows the `confirm` field to be accommodated in the `tap_action` along side the `service` tag, and follows the Home Assistant YAML format more closely.
|
||||
This allows the `confirm` and `pin` fields to be accommodated in the `tap_action` along side the `service` tag, and follows the Home Assistant YAML format more closely.
|
||||
|
||||
### More Examples
|
||||
|
||||
- [Switches](examples/Switches.md)
|
||||
- [Actions](examples/Actions.md)
|
||||
- [Templates](examples/Templates.md)
|
||||
* [Switches](examples/Switches.md)
|
||||
* [Actions](examples/Actions.md)
|
||||
* [Templates](examples/Templates.md)
|
||||
|
||||
## Editing the JSON file
|
||||
|
||||
@ -326,8 +329,8 @@ The `id` attribute values are taken from the same names used in [`strings.xml`](
|
||||
|
||||
6. We are unable to support Edge 540, Edge 840 and Edge 1050 devices at this time. The simulation of these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed.
|
||||
|
||||
7. We are unable to support HTTP without HTTPS. This is a limitation placed upon us by the Connect IQ API which for security reasons refuses to work with HTTP requests. There is nothing developers can do about this limitation. You will have to put an HTTPS proxy in front of your local Home Assistant to work with this application. See the [Trouble Shooting](TroubleShooting.md#do-it-yourself-setup) guide for an example setup. We would appreciate it if users did not leave poor reviews for the lack of this feature which is beyond our control to fix.
|
||||
7. We are unable to support HTTP natively (without the workaround specified earlier). This is a limitation placed upon us by the Connect IQ API which for security reasons refuses to work with HTTP requests. There is nothing developers can do about this limitation. See the [Trouble Shooting](TroubleShooting.md#do-it-yourself-setup) guide for an example setup. We would appreciate it if users did not leave poor reviews for the lack of this feature which is beyond our control to fix.
|
||||
|
||||
8. There is a [bug in Garmin Express so that when you use that software to amend the application's settings](https://github.com/house-of-abbey/GarminHomeAssistant/issues/194), the page appears in a random language not of your choice. I would like to thank user [heviiguy](https://github.com/heviiguy) for his work researching the issue, leading to these references that indicate the authors of Garmin Home Assistant cannot resolve this issue as its a bug in Garmin Express that Garmin are refusing to believe exists! See these pages for details:
|
||||
- [Garmin Express - Wrong Language](https://forums.garmin.com/developer/connect-iq/i/bug-reports/garmin-express---wrong-language)
|
||||
- [Incorrect language displayed for custom data fields](https://forums.garmin.com/developer/connect-iq/f/discussion/388137/incorrect-language-displayed-for-custom-data-fields)
|
||||
- [Incorrect language displayed for custom data fields](https://forums.garmin.com/developer/connect-iq/f/discussion/388137/incorrect-language-displayed-for-custom-data-fields)
|
||||
|
@ -120,6 +120,32 @@ There's an online way of testing the API URL too, thanks to [REQBIN](https://req
|
||||
|
||||

|
||||
|
||||
#### SSL Certificate Chain
|
||||
|
||||
With thanks to [@ziceva](https://github.com/ziceva) for solving this problem. The symptoms are:
|
||||
1. Using an API URL with SSL (HTTPS), the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) running in a browser on the same phone running Garmin Connect works well.
|
||||
2. The exact same configuration is set in the Garmin Home Assistant application.
|
||||
3. The Garmin Home Assistant application reports:
|
||||
```
|
||||
API: not available
|
||||
Menu: not available
|
||||
```
|
||||
|
||||
**Solution: Make sure you use a _full chain_ certificate in your HTTPS proxy as some watches might be unable to validate the site certificate alone.**
|
||||
|
||||
Most web browsers and OSes probably have the latest Certificate Authorities (CAs) trusted out-of-the-box and so they do not need the full chain to verify the certificates. Garmin watches may not have the latest CAs and that is why the Troubleshooting web page does not catch this problem. E.g. It turns out some Garmin watches do not have the LetsEncrypt CA marked as trusted.
|
||||
|
||||
To verify if you have this issue you can use a tool like [SSL Shoppers's SSL Checker](https://www.sslshopper.com/ssl-checker.html), which will catch this issue. The following two screen captures illustrate the difference between partial and full chain certificates respectively.
|
||||
|
||||
##### Partial Chain Certificate
|
||||
|
||||

|
||||
|
||||
##### Full Chain Certificate
|
||||
|
||||

|
||||
|
||||
|
||||
### Top Problems
|
||||
|
||||
1. Failure to copy & paste keys and URLs leading to minor and hard to see errors in strings, even with protestations they are the same! (No they weren't...)
|
||||
|
@ -26,63 +26,70 @@ rem SDK_PATH should work for all users
|
||||
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
|
||||
set SDK_PATH=%SDK_PATH:~0,-1%\bin
|
||||
rem Assume we can create and use this directory
|
||||
set DEST=export
|
||||
set DEST=bin
|
||||
|
||||
rem Device for simulation
|
||||
set DEVICE=venu2
|
||||
set JUNGLE=monkey.jungle
|
||||
|
||||
rem C:\>java -jar %SDK_PATH%\monkeybrains.jar -h
|
||||
rem usage: monkeyc [-a <arg>] [-b <arg>] [--build-stats <arg>] [-c <arg>] [-d <arg>]
|
||||
rem [--debug-log-level <arg>] [--debug-log-output <arg>] [-e]
|
||||
rem usage: monkeyc [-a <arg>] [-b <arg>] [--build-stats <arg>] [-d <arg>]
|
||||
rem [--debug-log-level <arg>] [--debug-log-output <arg>]
|
||||
rem [--disable-api-has-check-removal] [--disable-v2-opcodes] [-e]
|
||||
rem [--Eno-invalid-symbol] [-f <arg>] [-g] [-h] [-i <arg>] [-k] [-l <arg>]
|
||||
rem [-m <arg>] [--no-gen-styles] [-o <arg>] [-O <arg>] [-p <arg>] [-r] [-s
|
||||
rem <arg>] [-t] [-u <arg>] [-v] [-w] [-x <arg>] [-y <arg>] [-z <arg>]
|
||||
rem -a,--apidb <arg> API import file
|
||||
rem -b,--apimir <arg> API MIR file
|
||||
rem --build-stats <arg> Print build stats [0=basic]
|
||||
rem -c,--api-level <arg> API Level to target
|
||||
rem -d,--device <arg> Target device
|
||||
rem --debug-log-level <arg> Debug logging verbosity [0=errors, 1=basic,
|
||||
rem 2=intermediate, 3=verbose]
|
||||
rem --debug-log-output <arg>Output log zip file
|
||||
rem -e,--package-app Create an application package.
|
||||
rem --Eno-invalid-symbol Do not error when a symbol is found to be invalid
|
||||
rem -f,--jungles <arg> Jungle files
|
||||
rem -g,--debug Print debug output
|
||||
rem -h,--help Prints help information
|
||||
rem -i,--import-dbg <arg> Import api.debug.xml
|
||||
rem -k,--profile Enable profiling support
|
||||
rem -l,--typecheck <arg> Type check [0=off, 1=gradual, 2=informative,
|
||||
rem 3=strict]
|
||||
rem -m,--manifest <arg> Manifest file (deprecated)
|
||||
rem --no-gen-styles Do not generate Rez.Styles module
|
||||
rem -o,--output <arg> Output file to create
|
||||
rem -O,--optimization <arg> Optimization level [0=none, 1=basic, 2=fast
|
||||
rem optimizations, 3=slow optimizations] [p=optimize
|
||||
rem performance, z=optimize code space]
|
||||
rem -p,--project-info <arg> projectInfo.xml file to use when compiling
|
||||
rem -r,--release Strip debug information
|
||||
rem -s,--sdk-version <arg> SDK version to target (deprecated, use -c
|
||||
rem -t,--unit-test Enables compilation of unit tests
|
||||
rem -u,--devices <arg> devices.xml file to use when compiling (deprecated)
|
||||
rem -v,--version Prints the compiler version
|
||||
rem -w,--warn Show compiler warnings
|
||||
rem -x,--excludes <arg> Add annotations to the exclude list (deprecated)
|
||||
rem -y,--private-key <arg> Private key to sign builds with
|
||||
rem -z,--rez <arg> Resource files (deprecated)
|
||||
rem [-m <arg>] [--no-gen-styles] [-o <arg>] [-O <arg>] [-p <arg>] [-r] [-t]
|
||||
rem [-u <arg>] [-v] [-w] [-x <arg>] [-y <arg>] [-z <arg>]
|
||||
rem -a,--apidb <arg> API import file
|
||||
rem -b,--apimir <arg> API MIR file
|
||||
rem --build-stats <arg> Print build stats [0=basic]
|
||||
rem -d,--device <arg> Target device
|
||||
rem --debug-log-level <arg> Debug logging verbosity [0=errors, 1=basic,
|
||||
rem 2=intermediate, 3=verbose]
|
||||
rem --debug-log-output <arg> Output log zip file
|
||||
rem --disable-api-has-check-removalDo not optimize out API has checks
|
||||
rem --disable-v2-opcodes Do not use the v2 opcodes
|
||||
rem -e,--package-app Create an application package.
|
||||
rem --Eno-invalid-symbol Do not error when a symbol is found to be
|
||||
rem invalid
|
||||
rem -f,--jungles <arg> Jungle files
|
||||
rem -g,--debug Print debug output
|
||||
rem -h,--help Prints help information
|
||||
rem -i,--import-dbg <arg> Import api.debug.xml
|
||||
rem -k,--profile Enable profiling support
|
||||
rem -l,--typecheck <arg> Type check [0=off, 1=gradual, 2=informative,
|
||||
rem 3=strict]
|
||||
rem -m,--manifest <arg> Manifest file (deprecated)
|
||||
rem --no-gen-styles Do not generate Rez.Styles module
|
||||
rem -o,--output <arg> Output file to create
|
||||
rem -O,--optimization <arg> Optimization level [0=none, 1=basic, 2=fast
|
||||
rem optimizations, 3=slow optimizations]
|
||||
rem [p=optimize performance, z=optimize code
|
||||
rem space]
|
||||
rem -p,--project-info <arg> projectInfo.xml file to use when compiling
|
||||
rem -r,--release Strip debug information
|
||||
rem -t,--unit-test Enables compilation of unit tests
|
||||
rem -u,--devices <arg> devices.xml file to use when compiling
|
||||
rem (deprecated)
|
||||
rem -v,--version Prints the compiler version
|
||||
rem -w,--warn Show compiler warnings
|
||||
rem -x,--excludes <arg> Add annotations to the exclude list
|
||||
rem (deprecated)
|
||||
rem -y,--private-key <arg> Private key to sign builds with
|
||||
rem -z,--rez <arg> Resource files (deprecated)
|
||||
|
||||
title Compiling for %DEVICE%
|
||||
|
||||
rem Batch file's directory where the source code is
|
||||
set SRC=%~dp0
|
||||
rem drop last character '\'
|
||||
set SRC=%SRC:~0,-1%
|
||||
|
||||
if not exist %DEST% (
|
||||
md %DEST%
|
||||
if exist %DEST% (
|
||||
rmdir /s /q %DEST%
|
||||
)
|
||||
|
||||
if exist %SRC%\export\HomeAssistant*.iq (
|
||||
del /f /q %SRC%\export\HomeAssistant*.iq
|
||||
rem The above may not successfully delete the directory if there are locked files
|
||||
if not exist %DEST% (
|
||||
mkdir %DEST%
|
||||
)
|
||||
|
||||
echo.
|
||||
|
@ -70,7 +70,7 @@
|
||||
"const": "template",
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'tap' instead."
|
||||
"description": "Use 'info' or 'tap' instead."
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
@ -93,7 +93,7 @@
|
||||
"const": "template",
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'tap' instead."
|
||||
"description": "Use 'info' or 'tap' instead."
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
@ -104,6 +104,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "info"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"tap": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -169,7 +186,7 @@
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'toggle' or 'group'."
|
||||
"description": "One of 'info', 'tap', 'toggle' or 'group'."
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
@ -185,6 +202,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/tap"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/info"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/group"
|
||||
}
|
||||
@ -192,7 +212,7 @@
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"title": "Your familiar name to display in the menu item",
|
||||
"type": "string"
|
||||
},
|
||||
"entity": {
|
||||
|
@ -70,3 +70,8 @@ Note that for notify events, you _must_ not supply an `entity_id` or the API cal
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Be careful with the value of the `service` field.
|
||||
|
||||
Note that the `service` field will need to be a locally custom `script.<something>` as soon as any `data` fields are populated and not something more generic like `script.turn_on`. If the `service` field is wrong, the application will fail with a [`Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html) error in the response from your Home Assistant and show the error message as _"No JSON returned from HTTP request"_ on your device. In the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) you can use the standard developer tools to observe an `HTTP 400` error which the application does not see. Here we are limited by the [Garmin Connect IQ](https://developer.garmin.com/connect-iq/overview/) software development kit (SDK). We do not have enough information at the point of execution in the application to determine the cause of the error. Nor is there an immediately obvious way of identifying this issue using the JSON schema checks.
|
||||
|
@ -20,7 +20,7 @@ In this example we get the battery level of the device and add the percent sign.
|
||||
```json
|
||||
{
|
||||
"name": "Phone",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.<device>_battery_level') }}%"
|
||||
}
|
||||
```
|
||||
@ -32,17 +32,17 @@ The first two keep to the simple proposal above. The last combines them into a s
|
||||
```json
|
||||
{
|
||||
"name": "Hall Temp",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.hallway_temperature') }}°C"
|
||||
},
|
||||
{
|
||||
"name": "Hall Humidity",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.hallway_humidity') }}%"
|
||||
},
|
||||
{
|
||||
"name": "Hallway",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.hallway_temperature') }}°C {{ states('sensor.hallway_humidity') }}%"
|
||||
}
|
||||
```
|
||||
@ -52,7 +52,7 @@ In order to keep the formatting of floating point numbers under control, you mig
|
||||
```json
|
||||
{
|
||||
"name": "Hallway",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "T:{{ '%.1f' | format(states('sensor.hallway_temperature') | float) }}°C, H:{{ '%.1f' | format(states('sensor.hallway_humidity') | float) }}%"
|
||||
},
|
||||
```
|
||||
@ -62,12 +62,12 @@ Where your device supports unicode characters these example may work.
|
||||
```json
|
||||
{
|
||||
"name": "Charge",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "☎ {{ states('sensor.my_phone_battery_level') }}%{% if is_state('binary_sensor.my_phone_is_charging', 'on') %}⚡{% endif %}, ⏳ {{ '%.0f'|format(states('sensor.my_watch_battery_level') | float) }}%{% if is_state('binary_binary_sensor.my_watch_battery_is_charging', 'on') %}⚡{% endif %}"
|
||||
},
|
||||
{
|
||||
"name": "Hallway",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "🌡{% if is_state('sensor.hallway_temperature', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_temperature')|float) }}°C{% if is_state_attr('climate.hallway', 'hvac_action', 'heating') or is_state_attr('climate.hallway', 'hvac_action', 'preheating') -%}🔥{%- endif %}{% endif %}, 💧{% if is_state('sensor.hallway_humidity', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_humidity')|float) }}%{% endif %}"
|
||||
}
|
||||
```
|
||||
@ -83,7 +83,7 @@ In this example we get the battery level of the device and add the percent sign.
|
||||
```json
|
||||
{
|
||||
"name": "Phone",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %}"
|
||||
}
|
||||
```
|
||||
@ -93,7 +93,7 @@ Here we also use the else clause as well to give proper text instead of just `on
|
||||
```json
|
||||
{
|
||||
"name": "Garage Doors",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{% if is_state('binary_sensor.<door-0>', 'on') %}Open{% else %}Closed{% endif %} {% if is_state('binary_sensor.<door-1>', 'on') %}Open{% else %}Closed{% endif %}"
|
||||
}
|
||||
```
|
||||
@ -113,7 +113,7 @@ Note: Only when you use the `tap_action` field do you also need to include the `
|
||||
{
|
||||
"entity": "cover.garage_door",
|
||||
"name": "Garage Door",
|
||||
"type": "template",
|
||||
"type": "tap",
|
||||
"content": "{% if is_state('binary_sensor.garage_connected', 'on') %}{{state_translated('cover.garage_door')}} - {{state_attr('cover.garage_door', 'current_position')}}%{%else%}Unconnected{% endif %}",
|
||||
"tap_action": {
|
||||
"service": "cover.toggle",
|
||||
@ -150,7 +150,7 @@ Here we generate a bar graph of the battery level. We use the following steps to
|
||||
```json
|
||||
{
|
||||
"name": "Phone",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %} {{ '#' * (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int) }}{{ '_' * (<width> - (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int)) }}"
|
||||
}
|
||||
```
|
||||
@ -164,13 +164,13 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
"items": [
|
||||
{
|
||||
"name": "LEDs",
|
||||
"type": "template",
|
||||
"type": "info",
|
||||
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}"
|
||||
},
|
||||
{
|
||||
"entity": "light.green_house",
|
||||
"name": "LEDs 0",
|
||||
"type": "template",
|
||||
"type": "tap",
|
||||
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}",
|
||||
"tap_action": {
|
||||
"service": "light.turn_on",
|
||||
@ -193,7 +193,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
{
|
||||
"entity": "light.green_house",
|
||||
"name": "LEDs 2",
|
||||
"type": "template",
|
||||
"type": "tap",
|
||||
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}",
|
||||
"tap_action": {
|
||||
"service": "light.turn_on",
|
||||
@ -205,7 +205,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
{
|
||||
"entity": "light.green_house",
|
||||
"name": "LEDs 3",
|
||||
"type": "template",
|
||||
"type": "tap",
|
||||
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0))| int }}%{% else %}Off{% endif %}",
|
||||
"tap_action": {
|
||||
"service": "light.turn_on",
|
||||
|
81
export.cmd
@ -29,45 +29,49 @@ rem Assume we can create and use this directory
|
||||
set DEST=export
|
||||
set IQ=HomeAssistant-app.iq
|
||||
|
||||
rem C:\>java -jar %SDK_PATH%\monkeybrains.jar -h
|
||||
rem usage: monkeyc [-a <arg>] [-b <arg>] [--build-stats <arg>] [-c <arg>] [-d <arg>]
|
||||
rem [--debug-log-level <arg>] [--debug-log-output <arg>] [-e]
|
||||
rem usage: monkeyc [-a <arg>] [-b <arg>] [--build-stats <arg>] [-d <arg>]
|
||||
rem [--debug-log-level <arg>] [--debug-log-output <arg>]
|
||||
rem [--disable-api-has-check-removal] [--disable-v2-opcodes] [-e]
|
||||
rem [--Eno-invalid-symbol] [-f <arg>] [-g] [-h] [-i <arg>] [-k] [-l <arg>]
|
||||
rem [-m <arg>] [--no-gen-styles] [-o <arg>] [-O <arg>] [-p <arg>] [-r] [-s
|
||||
rem <arg>] [-t] [-u <arg>] [-v] [-w] [-x <arg>] [-y <arg>] [-z <arg>]
|
||||
rem -a,--apidb <arg> API import file
|
||||
rem -b,--apimir <arg> API MIR file
|
||||
rem --build-stats <arg> Print build stats [0=basic]
|
||||
rem -c,--api-level <arg> API Level to target
|
||||
rem -d,--device <arg> Target device
|
||||
rem --debug-log-level <arg> Debug logging verbosity [0=errors, 1=basic,
|
||||
rem 2=intermediate, 3=verbose]
|
||||
rem --debug-log-output <arg>Output log zip file
|
||||
rem -e,--package-app Create an application package.
|
||||
rem --Eno-invalid-symbol Do not error when a symbol is found to be invalid
|
||||
rem -f,--jungles <arg> Jungle files
|
||||
rem -g,--debug Print debug output
|
||||
rem -h,--help Prints help information
|
||||
rem -i,--import-dbg <arg> Import api.debug.xml
|
||||
rem -k,--profile Enable profiling support
|
||||
rem -l,--typecheck <arg> Type check [0=off, 1=gradual, 2=informative,
|
||||
rem 3=strict]
|
||||
rem -m,--manifest <arg> Manifest file (deprecated)
|
||||
rem --no-gen-styles Do not generate Rez.Styles module
|
||||
rem -o,--output <arg> Output file to create
|
||||
rem -O,--optimization <arg> Optimization level [0=none, 1=basic, 2=fast
|
||||
rem optimizations, 3=slow optimizations] [p=optimize
|
||||
rem performance, z=optimize code space]
|
||||
rem -p,--project-info <arg> projectInfo.xml file to use when compiling
|
||||
rem -r,--release Strip debug information
|
||||
rem -s,--sdk-version <arg> SDK version to target (deprecated, use -c
|
||||
rem -t,--unit-test Enables compilation of unit tests
|
||||
rem -u,--devices <arg> devices.xml file to use when compiling (deprecated)
|
||||
rem -v,--version Prints the compiler version
|
||||
rem -w,--warn Show compiler warnings
|
||||
rem -x,--excludes <arg> Add annotations to the exclude list (deprecated)
|
||||
rem -y,--private-key <arg> Private key to sign builds with
|
||||
rem -z,--rez <arg> Resource files (deprecated)
|
||||
rem [-m <arg>] [--no-gen-styles] [-o <arg>] [-O <arg>] [-p <arg>] [-r] [-t]
|
||||
rem [-u <arg>] [-v] [-w] [-x <arg>] [-y <arg>] [-z <arg>]
|
||||
rem -a,--apidb <arg> API import file
|
||||
rem -b,--apimir <arg> API MIR file
|
||||
rem --build-stats <arg> Print build stats [0=basic]
|
||||
rem -d,--device <arg> Target device
|
||||
rem --debug-log-level <arg> Debug logging verbosity [0=errors, 1=basic,
|
||||
rem 2=intermediate, 3=verbose]
|
||||
rem --debug-log-output <arg> Output log zip file
|
||||
rem --disable-api-has-check-removalDo not optimize out API has checks
|
||||
rem --disable-v2-opcodes Do not use the v2 opcodes
|
||||
rem -e,--package-app Create an application package.
|
||||
rem --Eno-invalid-symbol Do not error when a symbol is found to be
|
||||
rem invalid
|
||||
rem -f,--jungles <arg> Jungle files
|
||||
rem -g,--debug Print debug output
|
||||
rem -h,--help Prints help information
|
||||
rem -i,--import-dbg <arg> Import api.debug.xml
|
||||
rem -k,--profile Enable profiling support
|
||||
rem -l,--typecheck <arg> Type check [0=off, 1=gradual, 2=informative,
|
||||
rem 3=strict]
|
||||
rem -m,--manifest <arg> Manifest file (deprecated)
|
||||
rem --no-gen-styles Do not generate Rez.Styles module
|
||||
rem -o,--output <arg> Output file to create
|
||||
rem -O,--optimization <arg> Optimization level [0=none, 1=basic, 2=fast
|
||||
rem optimizations, 3=slow optimizations]
|
||||
rem [p=optimize performance, z=optimize code
|
||||
rem space]
|
||||
rem -p,--project-info <arg> projectInfo.xml file to use when compiling
|
||||
rem -r,--release Strip debug information
|
||||
rem -t,--unit-test Enables compilation of unit tests
|
||||
rem -u,--devices <arg> devices.xml file to use when compiling
|
||||
rem (deprecated)
|
||||
rem -v,--version Prints the compiler version
|
||||
rem -w,--warn Show compiler warnings
|
||||
rem -x,--excludes <arg> Add annotations to the exclude list
|
||||
rem (deprecated)
|
||||
rem -y,--private-key <arg> Private key to sign builds with
|
||||
rem -z,--rez <arg> Resource files (deprecated)
|
||||
|
||||
title Exporting Garmin Home Assistant Application
|
||||
|
||||
@ -96,7 +100,6 @@ echo.
|
||||
-Dfile.encoding=UTF-8 ^
|
||||
-Dapple.awt.UIElement=true ^
|
||||
-jar %SDK_PATH%\monkeybrains.jar ^
|
||||
--api-level 3.1.0 ^
|
||||
--output %IQ% ^
|
||||
--jungles %SRC%\monkey.jungle ^
|
||||
--private-key %SRC%\..\developer_key ^
|
||||
|
BIN
images/HTTPS_full_chain.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
images/HTTPS_partial_chain.png
Normal file
After Width: | Height: | Size: 72 KiB |
19
manifest.xml
@ -24,19 +24,16 @@
|
||||
Use "Monkey C: Edit Application" from the Visual Studio Code command palette
|
||||
to update the application attributes.
|
||||
-->
|
||||
<!--
|
||||
Testing in VSCode requires monkey.jungle, so for convenience, swap between
|
||||
watch-app and widget by changing which of the next two lines are commented out
|
||||
-->
|
||||
<iq:application id="98c36259-498a-4458-9cef-74a273ad2bc3" type="watch-app" name="@Strings.AppName" entry="HomeAssistantApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="3.1.0">
|
||||
<!--
|
||||
Use the following from the Visual Studio Code comand palette to edit
|
||||
Use the following from the Visual Studio Code command palette to edit
|
||||
the build targets:
|
||||
"Monkey C: Set Products by Product Category" - Lets you add all products
|
||||
that belong to the same product category
|
||||
"Monkey C: Edit Products" - Lets you add or remove any product
|
||||
-->
|
||||
<iq:products>
|
||||
<iq:product id="approachs50"/>
|
||||
<iq:product id="approachs7042mm"/>
|
||||
<iq:product id="approachs7047mm"/>
|
||||
<iq:product id="d2air"/>
|
||||
@ -46,6 +43,8 @@
|
||||
<iq:product id="d2deltas"/>
|
||||
<iq:product id="d2mach1"/>
|
||||
<iq:product id="descentg1"/>
|
||||
<iq:product id="descentg2"/>
|
||||
<iq:product id="descentmk1"/>
|
||||
<iq:product id="descentmk2"/>
|
||||
<iq:product id="descentmk2s"/>
|
||||
<iq:product id="descentmk343mm"/>
|
||||
@ -105,6 +104,8 @@
|
||||
<iq:product id="fr265"/>
|
||||
<iq:product id="fr265s"/>
|
||||
<iq:product id="fr55"/>
|
||||
<iq:product id="fr57042mm"/>
|
||||
<iq:product id="fr57047mm"/>
|
||||
<iq:product id="fr645"/>
|
||||
<iq:product id="fr645m"/>
|
||||
<iq:product id="fr745"/>
|
||||
@ -113,11 +114,18 @@
|
||||
<iq:product id="fr945lte"/>
|
||||
<iq:product id="fr955"/>
|
||||
<iq:product id="fr965"/>
|
||||
<iq:product id="fr970"/>
|
||||
<iq:product id="gpsmap66"/>
|
||||
<iq:product id="gpsmap67"/>
|
||||
<iq:product id="instinct2"/>
|
||||
<iq:product id="instinct2s"/>
|
||||
<iq:product id="instinct2x"/>
|
||||
<iq:product id="instinct3amoled45mm"/>
|
||||
<iq:product id="instinct3amoled50mm"/>
|
||||
<iq:product id="instinct3solar45mm"/>
|
||||
<iq:product id="instinctcrossover"/>
|
||||
<iq:product id="instincte40mm"/>
|
||||
<iq:product id="instincte45mm"/>
|
||||
<iq:product id="legacyherocaptainmarvel"/>
|
||||
<iq:product id="legacyherofirstavenger"/>
|
||||
<iq:product id="legacysagadarthvader"/>
|
||||
@ -150,6 +158,7 @@
|
||||
<iq:product id="vivoactive4"/>
|
||||
<iq:product id="vivoactive4s"/>
|
||||
<iq:product id="vivoactive5"/>
|
||||
<iq:product id="vivoactive6"/>
|
||||
</iq:products>
|
||||
<!--
|
||||
Use "Monkey C: Edit Permissions" from the Visual Studio Code command
|
||||
|
@ -31,9 +31,11 @@ project.manifest = manifest.xml
|
||||
#
|
||||
# The icons need to scale as a ratio of screen size 48:416 pixels
|
||||
#
|
||||
# Icon 55 53 48 46 42 37 32 30 28 26 24 21 18
|
||||
# Screen 480 454 416 390 360 320 280 260 240 218 208 176 156
|
||||
# Icon 55 53 48 46 42 37 32 30 28 26 24 21 19 18
|
||||
# Screen 480 454 416 390 360 320 280 260 240 218 208 176 166 156
|
||||
|
||||
# Screen Size 390x390 launcher icon size 56x56
|
||||
approachs50.resourcePath = $(approachs50.resourcePath);resources-launcher-56-56;resources-icons-46
|
||||
# Screen Size 390x390 launcher icon size 70x70
|
||||
approachs7042mm.resourcePath = $(approachs7042mm.resourcePath);resources-launcher-70-70;resources-icons-46
|
||||
# Screen Size 454x454 launcher icon size 80x80
|
||||
@ -50,6 +52,10 @@ d2deltas.resourcePath = $(d2deltas.resourcePath);resources-launcher-40-40;resour
|
||||
d2mach1.resourcePath = $(d2mach1.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 176x176 launcher icon size 62x62
|
||||
descentg1.resourcePath = $(descentg1.resourcePath);resources-launcher-62-62;resources-icons-21
|
||||
# Screen Size 390x390 launcher icon size 60x60
|
||||
descentg2.resourcePath = $(descentg2.resourcePath);resources-launcher-60-60;resources-icons-46
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
descentmk1.resourcePath = $(descentmk1.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
descentmk2.resourcePath = $(descentmk2.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
@ -81,8 +87,7 @@ edgeexplore.resourcePath = $(edgeexplore.resourcePath);resources-launcher-36-36;
|
||||
edgeexplore2.resourcePath = $(edgeexplore2.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
enduro.resourcePath = $(enduro.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
enduro3.resourcePath = $(enduro3.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
enduro3.resourcePath = $(enduro3.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 416x416 launcher icon size 60x60
|
||||
epix2.resourcePath = $(epix2.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 390x390 launcher icon size 60x60
|
||||
@ -109,7 +114,6 @@ fenix6spro.resourcePath = $(fenix6spro.resourcePath);resources-launcher-40-40;re
|
||||
fenix6xpro.resourcePath = $(fenix6xpro.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 260x260 launcher icon size 40x40
|
||||
fenix7.resourcePath = $(fenix7.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
# Screen Size 260x260 launcher icon size 40x40
|
||||
fenix7pro.resourcePath = $(fenix7pro.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
fenix7pronowifi.resourcePath = $(fenix7pronowifi.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
@ -118,7 +122,6 @@ fenix7s.resourcePath = $(fenix7s.resourcePath);resources-launcher-40-40;resource
|
||||
fenix7spro.resourcePath = $(fenix7spro.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
fenix7x.resourcePath = $(fenix7x.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
fenix7xpro.resourcePath = $(fenix7xpro.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
fenix7xpronowifi.resourcePath = $(fenix7xpronowifi.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 416x416 launcher icon size 60x60
|
||||
@ -135,7 +138,6 @@ fenixchronos.resourcePath = $(fenixchronos.resourcePath);resources-launcher-36-3
|
||||
fenixe.resourcePath = $(fenixe.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 390 x 390 launcher icon size 54x54
|
||||
fr165.resourcePath = $(descentmk2s.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
# Screen Size 390 x 390 launcher icon size 54x54
|
||||
fr165m.resourcePath = $(descentmk2s.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
fr245.resourcePath = $(fr245.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
@ -151,6 +153,10 @@ fr265.resourcePath = $(fr265.resourcePath);resources-launcher-60-60;resources-ic
|
||||
fr265s.resourcePath = $(fr265s.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 208x208 launcher icon size 35x35
|
||||
fr55.resourcePath = $(fr55.resourcePath);resources-launcher-35-35;resources-icons-24
|
||||
# Screen Size 390x390 launcher icon size 54x54
|
||||
fr57042mm.resourcePath = $(fr57042mm.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
# Screen Size 454x454 launcher icon size 65x65
|
||||
fr57047mm.resourcePath = $(fr57047mm.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
fr645.resourcePath = $(fr645.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
fr645m.resourcePath = $(fr645m.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
@ -163,16 +169,28 @@ fr945lte.resourcePath = $(fr945lte.resourcePath);resources-launcher-40-40;resour
|
||||
fr955.resourcePath = $(fr955.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
# Screen Size 454x454 launcher icon size 65x65
|
||||
fr965.resourcePath = $(fr965.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
fr970.resourcePath = $(fr970.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 240x400 launcher icon size 38x33
|
||||
gpsmap66.resourcePath = $(gpsmap66.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
gpsmap67.resourcePath = $(gpsmap67.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
# Screen Size 176x176 launcher icon size 62x62
|
||||
instinct2.resourcePath = $(instinct2.resourcePath);resources-launcher-62-62;resources-icons-21
|
||||
instinct2.resourcePath = $(instinct2.resourcePath);resources-launcher-62-62;resources-icons-21-w
|
||||
# Screen Size 163x156 launcher icon size 54x54
|
||||
instinct2s.resourcePath = $(instinct2s.resourcePath);resources-launcher-54-54;resources-icons-18
|
||||
instinct2s.resourcePath = $(instinct2s.resourcePath);resources-launcher-54-54;resources-icons-18-w
|
||||
# Screen Size 176x176 launcher icon size 62x62
|
||||
instinct2x.resourcePath = $(instinct2x.resourcePath);resources-launcher-62-62;resources-icons-21
|
||||
instinct2x.resourcePath = $(instinct2x.resourcePath);resources-launcher-62-62;resources-icons-21-w
|
||||
# Screen Size 390x390 launcher icon size 60x60, but the icon size used here is reduced as the menu items were clipped.
|
||||
instinct3amoled45mm.resourcePath = $(instinct3amoled45mm.resourcePath);resources-launcher-60-60;resources-icons-32
|
||||
# Screen Size 416x416 launcher icon size 60x60, but the icon size used here is reduced as the menu items were clipped.
|
||||
instinct3amoled50mm.resourcePath = $(instinct3amoled50mm.resourcePath);resources-launcher-60-60;resources-icons-34
|
||||
# Screen Size 176x176 launcher icon size 62x62
|
||||
instinct3solar45mm.resourcePath = $(instinct3solar45mm.resourcePath);resources-launcher-62-62;resources-icons-18-w
|
||||
# Screen Size 176x176 launcher icon size 26x26
|
||||
instinctcrossover.resourcePath = $(instinctcrossover.resourcePath);resources-launcher-26-26;resources-icons-21
|
||||
instinctcrossover.resourcePath = $(instinctcrossover.resourcePath);resources-launcher-26-26;resources-icons-21-w
|
||||
# Screen Size 166x166 launcher icon size 52x52, but the icon size used here is reduced as the menu items were clipped.
|
||||
instincte40mm.resourcePath = $(instincte40mm.resourcePath);resources-launcher-52-52;resources-icons-18-w
|
||||
# Screen Size 176x176 launcher icon size 62x62, but the icon size used here is reduced as the menu items were clipped.
|
||||
instincte45mm.resourcePath = $(instincte45mm.resourcePath);resources-launcher-62-62;resources-icons-18-w
|
||||
# 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
|
||||
@ -211,7 +229,6 @@ venud.resourcePath = $(venud.resourcePath);resources-launcher-60-60;resources-ic
|
||||
venusq.resourcePath = $(venusq.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
# Screen Size 320x360 launcher icon size 40x40
|
||||
venusq2.resourcePath = $(venusq2.resourcePath);resources-launcher-40-40;resources-icons-38
|
||||
# Screen Size 320x360 launcher icon size 40x40
|
||||
venusq2m.resourcePath = $(venusq2m.resourcePath);resources-launcher-40-40;resources-icons-38
|
||||
# Screen Size 240x240 launcher icon size 36x36
|
||||
venusqm.resourcePath = $(venusqm.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
@ -223,5 +240,7 @@ vivoactive3mlte.resourcePath = $(vivoactive3mlte.resourcePath);resources-launche
|
||||
vivoactive4.resourcePath = $(vivoactive4.resourcePath);resources-launcher-35-35;resources-icons-30
|
||||
# Screen Size 218x218 launcher icon size 30x30
|
||||
vivoactive4s.resourcePath = $(vivoactive4s.resourcePath);resources-launcher-30-30;resources-icons-26
|
||||
# Screen Size 390x390 launcher icon size 70x70
|
||||
vivoactive5.resourcePath = $(vivoactive5.resourcePath);resources-launcher-70-70;resources-icons-46
|
||||
# Screen Size 390x390 launcher icon size 56x56
|
||||
vivoactive5.resourcePath = $(vivoactive5.resourcePath);resources-launcher-56-56;resources-icons-46
|
||||
# Screen Size 390x390 launcher icon size 54x54
|
||||
vivoactive6.resourcePath = $(vivoactive6.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
|
23
resources-icons-18-w/drawables.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
J D Abbey & P A Abbey, 28 December 2022
|
||||
|
||||
References:
|
||||
* https://fonts.google.com/icons
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="ErrorIcon" filename="error.svg"/>
|
||||
<bitmap id="GroupTypeIcon" filename="group_type.svg"/>
|
||||
<bitmap id="TapTypeIcon" filename="tap_type.svg"/>
|
||||
<bitmap id="InfoTypeIcon" filename="info_type.svg"/>
|
||||
</drawables>
|
1
resources-icons-18-w/error.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="18" viewBox="0 0 48 48" width="18" xmlns="http://www.w3.org/2000/svg"><path d="M24 34q.7 0 1.175-.475.475-.475.475-1.175 0-.7-.475-1.175Q24.7 30.7 24 30.7q-.7 0-1.175.475-.475.475-.475 1.175 0 .7.475 1.175Q23.3 34 24 34Zm-1.35-7.65h3V13.7h-3ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 23.95q0-4.1 1.575-7.75 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24.05 4q4.1 0 7.75 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.975T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24.05 41ZM24 24Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 716 B |
7
resources-icons-18-w/group_type.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg height="18" viewBox="0 0 200 500" width="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="0"/>
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="200"/>
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="400"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 364 B |
1
resources-icons-18-w/info_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="18" viewBox="0 -960 960 960" width="18" xmlns="http://www.w3.org/2000/svg"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 545 B |
1
resources-icons-18-w/tap_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="18" viewBox="0 -960 960 960" width="18" xmlns="http://www.w3.org/2000/svg"><path d="M445-80q-29 0-56-12t-45-35L127-403l21-23q14-15 34.5-18.5T221-438l99 53v-365q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T380-750v465l-144-77 156 198q10 12 23.76 18 13.76 6 29.24 6h205q38 0 64-26t26-64v-170q0-25.5-17.25-42.75T680-460H460v-60h219.646q50.148 0 85.251 35T800-400v170q0 63-43.5 106.5T650-80H445ZM203-665q-11.074-18.754-17.037-40.492Q180-727.229 180-750.246 180-821 229.725-870.5T350-920q70.55 0 120.275 49.738Q520-820.524 520-749.956q0 22.956-5.963 44.614Q508.074-683.685 497-665l-52-30q7-12 11-26t4-29.478Q460-796 427.882-828q-32.117-32-78-32Q304-860 272-827.917 240-795.833 240-750q0 15 4 29t11 26l-52 30Zm285 335Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 783 B |
23
resources-icons-21-w/drawables.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
J D Abbey & P A Abbey, 28 December 2022
|
||||
|
||||
References:
|
||||
* https://fonts.google.com/icons
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="ErrorIcon" filename="error.svg"/>
|
||||
<bitmap id="GroupTypeIcon" filename="group_type.svg"/>
|
||||
<bitmap id="TapTypeIcon" filename="tap_type.svg"/>
|
||||
<bitmap id="InfoTypeIcon" filename="info_type.svg"/>
|
||||
</drawables>
|
1
resources-icons-21-w/error.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="21" viewBox="0 0 48 48" width="21" xmlns="http://www.w3.org/2000/svg"><path d="M24 34q.7 0 1.175-.475.475-.475.475-1.175 0-.7-.475-1.175Q24.7 30.7 24 30.7q-.7 0-1.175.475-.475.475-.475 1.175 0 .7.475 1.175Q23.3 34 24 34Zm-1.35-7.65h3V13.7h-3ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 23.95q0-4.1 1.575-7.75 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24.05 4q4.1 0 7.75 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.975T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24.05 41ZM24 24Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 716 B |
7
resources-icons-21-w/group_type.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg height="21" viewBox="0 0 200 500" width="21" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="0"/>
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="200"/>
|
||||
<rect fill="white" height="100" rx="40" ry="40" width="200" x="0" y="400"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 364 B |
1
resources-icons-21-w/info_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="21" viewBox="0 -960 960 960" width="21" xmlns="http://www.w3.org/2000/svg"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 545 B |
1
resources-icons-21-w/tap_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="21" viewBox="0 -960 960 960" width="21" xmlns="http://www.w3.org/2000/svg"><path d="M445-80q-29 0-56-12t-45-35L127-403l21-23q14-15 34.5-18.5T221-438l99 53v-365q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T380-750v465l-144-77 156 198q10 12 23.76 18 13.76 6 29.24 6h205q38 0 64-26t26-64v-170q0-25.5-17.25-42.75T680-460H460v-60h219.646q50.148 0 85.251 35T800-400v170q0 63-43.5 106.5T650-80H445ZM203-665q-11.074-18.754-17.037-40.492Q180-727.229 180-750.246 180-821 229.725-870.5T350-920q70.55 0 120.275 49.738Q520-820.524 520-749.956q0 22.956-5.963 44.614Q508.074-683.685 497-665l-52-30q7-12 11-26t4-29.478Q460-796 427.882-828q-32.117-32-78-32Q304-860 272-827.917 240-795.833 240-750q0 15 4 29t11 26l-52 30Zm285 335Z" fill="white" stroke="white"/></svg>
|
After Width: | Height: | Size: 783 B |
23
resources-icons-34/drawables.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
J D Abbey & P A Abbey, 28 December 2022
|
||||
|
||||
References:
|
||||
* https://fonts.google.com/icons
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="ErrorIcon" filename="error.svg"/>
|
||||
<bitmap id="GroupTypeIcon" filename="group_type.svg"/>
|
||||
<bitmap id="TapTypeIcon" filename="tap_type.svg"/>
|
||||
<bitmap id="InfoTypeIcon" filename="info_type.svg"/>
|
||||
</drawables>
|
1
resources-icons-34/error.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="34" viewBox="0 0 48 48" width="34" xmlns="http://www.w3.org/2000/svg"><path d="M24 34q.7 0 1.175-.475.475-.475.475-1.175 0-.7-.475-1.175Q24.7 30.7 24 30.7q-.7 0-1.175.475-.475.475-.475 1.175 0 .7.475 1.175Q23.3 34 24 34Zm-1.35-7.65h3V13.7h-3ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 23.95q0-4.1 1.575-7.75 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24.05 4q4.1 0 7.75 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.975T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24.05 41ZM24 24Z" fill="red" stroke="red"/></svg>
|
After Width: | Height: | Size: 712 B |
7
resources-icons-34/group_type.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg height="34" viewBox="0 0 200 500" width="34" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="0"/>
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="200"/>
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="400"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 361 B |
1
resources-icons-34/info_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="34" viewBox="0 -960 960 960" width="34" xmlns="http://www.w3.org/2000/svg"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" fill="blue" stroke="blue"/></svg>
|
After Width: | Height: | Size: 543 B |
1
resources-icons-34/tap_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="34" viewBox="0 -960 960 960" width="34" xmlns="http://www.w3.org/2000/svg"><path d="M445-80q-29 0-56-12t-45-35L127-403l21-23q14-15 34.5-18.5T221-438l99 53v-365q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T380-750v465l-144-77 156 198q10 12 23.76 18 13.76 6 29.24 6h205q38 0 64-26t26-64v-170q0-25.5-17.25-42.75T680-460H460v-60h219.646q50.148 0 85.251 35T800-400v170q0 63-43.5 106.5T650-80H445ZM203-665q-11.074-18.754-17.037-40.492Q180-727.229 180-750.246 180-821 229.725-870.5T350-920q70.55 0 120.275 49.738Q520-820.524 520-749.956q0 22.956-5.963 44.614Q508.074-683.685 497-665l-52-30q7-12 11-26t4-29.478Q460-796 427.882-828q-32.117-32-78-32Q304-860 272-827.917 240-795.833 240-750q0 15 4 29t11 26l-52 30Zm285 335Z" fill="blue" stroke="blue"/></svg>
|
After Width: | Height: | Size: 781 B |
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 200 500" width="48" height="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<rect x="0" y="0" width="200" height="100" fill="blue" rx="40" ry="40"/>
|
||||
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 380 B |
17
resources-launcher-52-52/drawables.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher.svg" />
|
||||
</drawables>
|
4
resources-launcher-52-52/launcher.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg fill="none" height="52" viewBox="0 0 400 400" width="52" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z" fill="#F2F4F9"/>
|
||||
<path d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
17
resources-launcher-56-56/drawables.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher.svg" />
|
||||
</drawables>
|
4
resources-launcher-56-56/launcher.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg fill="none" height="56" viewBox="0 0 400 400" width="56" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z" fill="#F2F4F9"/>
|
||||
<path d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -11,15 +11,6 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Alert provides a means to present application notifications to the user
|
||||
// briefly. Credit to travis.vitek on forums.garmin.com.
|
||||
//
|
||||
// Reference:
|
||||
// * https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -27,6 +18,12 @@ using Toybox.Graphics;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Timer;
|
||||
|
||||
//! The Alert class provides a means to present application notifications to the user
|
||||
//! briefly. Credit to travis.vitek on forums.garmin.com.
|
||||
//!
|
||||
//! Reference:
|
||||
//! @url https://forums.garmin.com/developer/connect-iq/f/discussion/106/how-to-show-alert-messages
|
||||
//
|
||||
class Alert extends WatchUi.View {
|
||||
private static const scRadius = 10;
|
||||
private var mTimer as Timer.Timer;
|
||||
@ -36,6 +33,16 @@ class Alert extends WatchUi.View {
|
||||
private var mFgcolor as Graphics.ColorType;
|
||||
private var mBgcolor as Graphics.ColorType;
|
||||
|
||||
//! Class Constructor
|
||||
//! @param params A dictionary object as follows:<br>
|
||||
//! {<br>
|
||||
//!   :timeout as Lang.Number, // Timeout in millseconds<br>
|
||||
//!   :font as Graphics.FontType, // Text font size<br>
|
||||
//!   :text as Lang.String, // Text to display<br>
|
||||
//!   :fgcolor as Graphics.ColorType, // Foreground Colour<br>
|
||||
//!   :bgcolor as Graphics.ColorType // Background Colour<br>
|
||||
//! }
|
||||
//
|
||||
function initialize(params as Lang.Dictionary) {
|
||||
View.initialize();
|
||||
|
||||
@ -67,14 +74,22 @@ class Alert extends WatchUi.View {
|
||||
mTimer = new Timer.Timer();
|
||||
}
|
||||
|
||||
//! Setup a timer to dismiss the alert.
|
||||
//
|
||||
function onShow() {
|
||||
mTimer.start(method(:dismiss), mTimeout, false);
|
||||
}
|
||||
|
||||
//! Prematurely stop the timer.
|
||||
//
|
||||
function onHide() {
|
||||
mTimer.stop();
|
||||
}
|
||||
|
||||
//! Draw the Alert view.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onUpdate(dc as Graphics.Dc) {
|
||||
var tWidth = dc.getTextWidthInPixels(mText, mFont);
|
||||
var tHeight = dc.getFontHeight(mFont);
|
||||
@ -110,32 +125,49 @@ class Alert extends WatchUi.View {
|
||||
dc.drawText(tX, tY, mFont, mText, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
|
||||
// Remove the alert from view, usually on user input, but that is defined by the calling function.
|
||||
//! Remove the alert from view, usually on user input, but that is defined by the calling function.
|
||||
//
|
||||
function dismiss() as Void {
|
||||
WatchUi.popView(SLIDE_IMMEDIATE);
|
||||
}
|
||||
|
||||
function pushView(transition) as Void {
|
||||
//! Push this view onto the view stack.
|
||||
//!
|
||||
//! @param transition Slide Type
|
||||
function pushView(transition as WatchUi.SlideType) as Void {
|
||||
WatchUi.pushView(self, new AlertDelegate(self), transition);
|
||||
}
|
||||
}
|
||||
|
||||
//! Input Delegate for the Alert view.
|
||||
//
|
||||
class AlertDelegate extends WatchUi.InputDelegate {
|
||||
private var mView;
|
||||
private var mView as Alert;
|
||||
|
||||
function initialize(view) {
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param view The Alert view for which this class is a delegate.
|
||||
//!
|
||||
function initialize(view as Alert) {
|
||||
InputDelegate.initialize();
|
||||
mView = view;
|
||||
}
|
||||
|
||||
function onKey(evt) as Lang.Boolean {
|
||||
//! Handle key events.
|
||||
//!
|
||||
//! @param evt The key event whose value is ignored, just fact of key event matters.
|
||||
//!
|
||||
function onKey(evt as WatchUi.KeyEvent) as Lang.Boolean {
|
||||
mView.dismiss();
|
||||
getApp().getQuitTimer().reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onTap(evt) as Lang.Boolean {
|
||||
//! Handle click events.
|
||||
//!
|
||||
//! @param evt The click event whose value is ignored, just fact of key event matters.
|
||||
//!
|
||||
function onTap(evt as WatchUi.ClickEvent) as Lang.Boolean {
|
||||
mView.dismiss();
|
||||
getApp().getQuitTimer().reset();
|
||||
return true;
|
||||
|
@ -11,12 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// The background service delegate currently just reports the Garmin watch's battery
|
||||
// level.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -25,20 +19,43 @@ using Toybox.Background;
|
||||
using Toybox.System;
|
||||
using Toybox.Activity;
|
||||
|
||||
//! The background service delegate reports the Garmin watch's various status values
|
||||
//! back to the Home Assistant instance.
|
||||
//
|
||||
(:background)
|
||||
class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
ServiceDelegate.initialize();
|
||||
}
|
||||
|
||||
function onReturnBatteryUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Code: " + responseCode);
|
||||
// System.println("BackgroundServiceDelegate onReturnBatteryUpdate() Response Data: " + data);
|
||||
//! Callback function for doUpdate().
|
||||
//!
|
||||
//! @param responseCode Response code
|
||||
//! @param data Return data
|
||||
//
|
||||
function onReturnDoUpdate(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
// System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Code: " + responseCode);
|
||||
// System.println("BackgroundServiceDelegate onReturnDoUpdate() Response Data: " + data);
|
||||
Background.exit(null);
|
||||
}
|
||||
|
||||
function onActivityCompleted(activity as { :sport as Activity.Sport, :subSport as Activity.SubSport }) as Void {
|
||||
//! Called on completion of an activity.
|
||||
//!
|
||||
//! @param activity Specified as a Dictionary with two items.<br>
|
||||
//! {<br>
|
||||
//!   :sport as Activity.Sport<br>
|
||||
//!   :subSport as Activity.SubSport<br>
|
||||
//! }
|
||||
//
|
||||
function onActivityCompleted(
|
||||
activity as {
|
||||
:sport as Activity.Sport,
|
||||
:subSport as Activity.SubSport
|
||||
}
|
||||
) as Void {
|
||||
if (!System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call.");
|
||||
} else if (!System.getDeviceSettings().connectionAvailable) {
|
||||
@ -50,6 +67,8 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
//! Called periodically to send status updates to the Home Assistant instance.
|
||||
//
|
||||
function onTemporalEvent() as Void {
|
||||
if (!System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call.");
|
||||
@ -76,7 +95,15 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) {
|
||||
//! Combined update function to collect the data to be sent as updates to the Home Assistant instance.
|
||||
//!
|
||||
//! @param activity Activity.Sport
|
||||
//! @param sub_activity Activity.SubSport
|
||||
//
|
||||
private function doUpdate(
|
||||
activity as Lang.Number or Null,
|
||||
sub_activity as Lang.Number or Null
|
||||
) {
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
|
||||
var position = Position.getInfo();
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees());
|
||||
@ -136,7 +163,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnBatteryUpdate)
|
||||
method(:onReturnDoUpdate)
|
||||
);
|
||||
}
|
||||
var activityInfo = ActivityMonitor.getInfo();
|
||||
@ -222,7 +249,7 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnBatteryUpdate)
|
||||
method(:onReturnDoUpdate)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,15 +11,12 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// ClientId is somewhere to store personal credentials that should not be shared in
|
||||
// a separate file that is locally customised to the source code and not commited
|
||||
// back to GitHub.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
//! ClientId is somewhere to store personal credentials that should not be shared in
|
||||
//! a separate file that is locally customised to the source code and not committed
|
||||
//! back to GitHub.
|
||||
//
|
||||
(:glance)
|
||||
class ClientId {
|
||||
static const webLogUrl = "https://...";
|
||||
|
@ -11,22 +11,6 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// ErrorView provides a means to present application errors to the user. These
|
||||
// should not happen of course... but they do, so best make sure errors can be
|
||||
// reported.
|
||||
//
|
||||
// Designed so that a single ErrorView is used for all errors and hence can ensure
|
||||
// that only the first call to display is honoured until the view is dismissed.
|
||||
// This compensates for older devices not being able to call WatchUi.getCurrentView()
|
||||
// due to not supporting API level 3.4.0.
|
||||
//
|
||||
// Usage:
|
||||
// 1) ErrorView.show("Error message");
|
||||
// 2) return ErrorView.create("Error message"); // as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Graphics;
|
||||
@ -35,6 +19,19 @@ using Toybox.WatchUi;
|
||||
using Toybox.Communications;
|
||||
using Toybox.Timer;
|
||||
|
||||
//! ErrorView provides a means to present application errors to the user. These
|
||||
//! should not happen of course... but they do, so best make sure errors can be
|
||||
//! reported.
|
||||
//!
|
||||
//! Designed so that a single ErrorView is used for all errors and hence can ensure
|
||||
//! that only the first call to display is honoured until the view is dismissed.
|
||||
//! This compensates for older devices not being able to call WatchUi.getCurrentView()
|
||||
//! due to not supporting API level 3.4.0.
|
||||
//!
|
||||
//! Usage:
|
||||
//! 1) `ErrorView.show("Error message");`
|
||||
//! 2) `return ErrorView.create("Error message"); // as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>`
|
||||
//
|
||||
class ErrorView extends ScalableView {
|
||||
private static const scErrorIconMargin as Lang.Float = 7f;
|
||||
private var mText as Lang.String = "";
|
||||
@ -48,9 +45,11 @@ class ErrorView extends ScalableView {
|
||||
private static var instance;
|
||||
private static var mShown as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
ScalableView.initialize();
|
||||
mDelegate = new ErrorDelegate(self);
|
||||
mDelegate = new ErrorDelegate();
|
||||
// Convert the settings from % of screen size to pixels
|
||||
mErrorIconMargin = pixelsForScreen(scErrorIconMargin);
|
||||
mErrorIcon = Application.loadResource(Rez.Drawables.ErrorIcon) as Graphics.BitmapResource;
|
||||
@ -59,7 +58,10 @@ class ErrorView extends ScalableView {
|
||||
}
|
||||
}
|
||||
|
||||
// Load your resources here
|
||||
//! Construct the view.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onLayout(dc as Graphics.Dc) as Void {
|
||||
var w = dc.getWidth();
|
||||
|
||||
@ -75,7 +77,10 @@ class ErrorView extends ScalableView {
|
||||
});
|
||||
}
|
||||
|
||||
// Update the view
|
||||
//! Update the view
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onUpdate(dc as Graphics.Dc) as Void {
|
||||
var w = dc.getWidth();
|
||||
if (mAntiAlias) {
|
||||
@ -87,10 +92,18 @@ class ErrorView extends ScalableView {
|
||||
mTextArea.draw(dc);
|
||||
}
|
||||
|
||||
//! Get this view's delegate for processing events.
|
||||
//
|
||||
function getDelegate() as ErrorDelegate {
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
//! 'Create' (get) the ErrorView instance, intended to make short work of using this class. E.g.
|
||||
//!
|
||||
//! `return ErrorView.create("Went wrong!");`
|
||||
//!
|
||||
//! @param text The string to display in the ErrorView.
|
||||
//
|
||||
static function create(text as Lang.String) as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
|
||||
if (instance == null) {
|
||||
instance = new ErrorView();
|
||||
@ -102,7 +115,10 @@ class ErrorView extends ScalableView {
|
||||
return [instance, instance.getDelegate()];
|
||||
}
|
||||
|
||||
// Create or reuse an existing ErrorView, and pass on the text.
|
||||
//! Create or reuse an existing ErrorView, and pass on the text.
|
||||
//!
|
||||
//! @param text The string to display in the ErrorView.
|
||||
//
|
||||
static function show(text as Lang.String) as Void {
|
||||
if (!mShown) {
|
||||
create(text); // Ignore returned values
|
||||
@ -113,6 +129,8 @@ class ErrorView extends ScalableView {
|
||||
}
|
||||
}
|
||||
|
||||
//! Pop the view and clean up timers.
|
||||
//
|
||||
static function unShow() as Void {
|
||||
if (mShown) {
|
||||
WatchUi.popView(WatchUi.SLIDE_DOWN);
|
||||
@ -126,7 +144,10 @@ class ErrorView extends ScalableView {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal show now we're not a static method like 'show()'.
|
||||
//! Internal show now we're not a static method like 'show()'.
|
||||
//!
|
||||
//! @param text Change the string tio display in the ErrorView.
|
||||
//
|
||||
function setText(text as Lang.String) as Void {
|
||||
mText = text;
|
||||
if (mTextArea != null) {
|
||||
@ -137,12 +158,19 @@ class ErrorView extends ScalableView {
|
||||
|
||||
}
|
||||
|
||||
|
||||
//! Delegate for the ErrorView.
|
||||
//
|
||||
class ErrorDelegate extends WatchUi.BehaviorDelegate {
|
||||
|
||||
function initialize(view as ErrorView) {
|
||||
//! Class Constructor
|
||||
//!
|
||||
function initialize() {
|
||||
WatchUi.BehaviorDelegate.initialize();
|
||||
}
|
||||
|
||||
//! Process the event to clear the ErrorView.
|
||||
//
|
||||
function onBack() as Lang.Boolean {
|
||||
getApp().getQuitTimer().reset();
|
||||
ErrorView.unShow();
|
||||
|
@ -9,32 +9,40 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant centralised constants.
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
|
||||
//! Home Assistant centralised constants.
|
||||
//
|
||||
(:glance)
|
||||
class Globals {
|
||||
//! Alert is a toast at the top of the watch screen, it stays present until tapped
|
||||
//! or this timeout has expired.
|
||||
static const scAlertTimeout = 2000; // ms
|
||||
static const scTapTimeout = 1000; // ms
|
||||
// Time to let the existing HTTP responses get serviced after a
|
||||
// Communications.NETWORK_RESPONSE_OUT_OF_MEMORY response code.
|
||||
|
||||
//! Time to let the existing HTTP responses get serviced after a
|
||||
//! `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` response code.
|
||||
static const scApiBackoff = 1000; // ms
|
||||
// Needs to be long enough to enable a "double ESC" to quit the application from
|
||||
// an ErrorView.
|
||||
|
||||
//! Needs to be long enough to enable a "double ESC" to quit the application from
|
||||
//! an ErrorView.
|
||||
static const scApiResume = 200; // ms
|
||||
// Warn the user after fetching the menu if their watch is low on memory before the device crashes.
|
||||
|
||||
//! Warn the user after fetching the menu if their watch is low on memory before the device crashes.
|
||||
static const scLowMem = 0.90; // percent as a fraction.
|
||||
|
||||
// Constants for PIN confirmation dialog
|
||||
static const scPinMaxFailures = 5; // Maximum number of failed PIN confirmation attemps allwed in ...
|
||||
static const scPinMaxFailureMinutes = 2; // ... this number of minutes before PIN confirmation is locked for ...
|
||||
static const scPinLockTimeMinutes = 10; // ... this number of minutes
|
||||
//! Constant for PIN confirmation dialog.<br>
|
||||
//! Maximum number of failed PIN confirmation attempts allowed in `scPinMaxFailureMinutes`.
|
||||
static const scPinMaxFailures = 5;
|
||||
|
||||
//! Constant for PIN confirmation dialog.<br>
|
||||
//! Period in minutes during which no more than `scPinMaxFailures` PIN attempts are tolerated.
|
||||
static const scPinMaxFailureMinutes = 2;
|
||||
|
||||
//! Constant for PIN confirmation dialog.<br>
|
||||
//! Lock out time in minutes after a failed PIN entry.
|
||||
static const scPinLockTimeMinutes = 10;
|
||||
}
|
||||
|
@ -9,12 +9,7 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Application root for GarminHomeAssistant
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -25,6 +20,8 @@ using Toybox.System;
|
||||
using Toybox.Application.Properties;
|
||||
using Toybox.Timer;
|
||||
|
||||
//! Application root for GarminHomeAssistant
|
||||
//
|
||||
(:glance, :background)
|
||||
class HomeAssistantApp extends Application.AppBase {
|
||||
private var mApiStatus as Lang.String or Null;
|
||||
@ -39,8 +36,9 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
||||
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||
private var mTemplates as Lang.Dictionary = {};
|
||||
private var startUpdating as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
// ATTENTION when adding stuff into this block:
|
||||
@ -56,7 +54,10 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
// with "(:glance)".
|
||||
}
|
||||
|
||||
// onStart() is called on application start up
|
||||
//! Called on application start up
|
||||
//!
|
||||
//! @param state see `AppBase.onStart()`
|
||||
//
|
||||
function onStart(state as Lang.Dictionary?) as Void {
|
||||
AppBase.onStart(state);
|
||||
// ATTENTION when adding stuff into this block:
|
||||
@ -72,7 +73,11 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
// with "(:glance)".
|
||||
}
|
||||
|
||||
// onStop() is called when your application is exiting
|
||||
//! Called when your application is exiting
|
||||
//
|
||||
//!
|
||||
//! @param state see `AppBase.onStop()`
|
||||
//
|
||||
function onStop(state as Lang.Dictionary?) as Void {
|
||||
AppBase.onStop(state);
|
||||
// ATTENTION when adding stuff into this block:
|
||||
@ -88,7 +93,10 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
// with "(:glance)".
|
||||
}
|
||||
|
||||
// Return the initial view of your application here
|
||||
//! Returns the initial view of the application.
|
||||
//!
|
||||
//! @return The initial view.
|
||||
//
|
||||
function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
|
||||
mIsApp = true;
|
||||
mQuitTimer = new QuitTimer();
|
||||
@ -120,19 +128,29 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
} else {
|
||||
var isCached = fetchMenuConfig();
|
||||
var ret = null;
|
||||
fetchApiStatus();
|
||||
if (isCached) {
|
||||
return [mHaMenu, new HomeAssistantViewDelegate(true)];
|
||||
ret = [mHaMenu, new HomeAssistantViewDelegate(true)];
|
||||
} else {
|
||||
return [new WatchUi.View(), new WatchUi.BehaviorDelegate()];
|
||||
ret = [new WatchUi.View(), new WatchUi.BehaviorDelegate()];
|
||||
}
|
||||
// Separated from Settings.update() in order to call after fetchMenuConfig() and not call it on changes settings.
|
||||
Settings.webhook();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the configuration menu.
|
||||
//! Callback function after completing the GET request to fetch the configuration menu.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
(:glance)
|
||||
function onReturnFetchMenuConfig(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
function onReturnFetchMenuConfig(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String
|
||||
) as Void {
|
||||
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantApp onReturnFetchMenuConfig() Response Data: " + data);
|
||||
|
||||
@ -205,11 +223,14 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
// Return true if the menu came from the cache, otherwise false. This is because fetching the menu when not in the cache is
|
||||
// asynchronous and affects how the views are managed.
|
||||
//! Fetch the menu configuration over HTTPS, which might be locally cached.
|
||||
//!
|
||||
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
|
||||
//! the menu when not in the cache is asynchronous and affects how the views are managed.
|
||||
//
|
||||
(:glance)
|
||||
function fetchMenuConfig() as Lang.Boolean {
|
||||
// System.println("URL = " + Settings.getConfigUrl());
|
||||
// System.println("Menu URL = " + Settings.getConfigUrl());
|
||||
if (Settings.getConfigUrl().equals("")) {
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||
WatchUi.requestUpdate();
|
||||
@ -260,23 +281,37 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
//! Build the menu and store in `mHaMenu`. Then start updates if necessary.
|
||||
//!
|
||||
//! @param menu The dictionary derived from the JSON menu fetched by `fetchMenuConfig()`.
|
||||
//
|
||||
private function buildMenu(menu as Lang.Dictionary) {
|
||||
mHaMenu = new HomeAssistantView(menu, null);
|
||||
mQuitTimer.begin();
|
||||
if (startUpdating) {
|
||||
if (!Settings.getWebhookId().equals("")) {
|
||||
startUpdates();
|
||||
}
|
||||
} // If not, this will be done via a chain in Settings.webhook() and mWebhookManager.requestWebhookId() that registers the sensors.
|
||||
}
|
||||
|
||||
//! Start the periodic menu updates for as long as the application is running.
|
||||
//
|
||||
function startUpdates() {
|
||||
if (mHaMenu != null and !mUpdating) {
|
||||
// Start the continuous update process that continues for as long as the application is running.
|
||||
updateMenuItems();
|
||||
mUpdating = true;
|
||||
}
|
||||
startUpdating = true;
|
||||
}
|
||||
|
||||
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
|
||||
//! Callback function for each menu update GET request.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
function onReturnUpdateMenuItems(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary
|
||||
) as Void {
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
|
||||
|
||||
@ -350,6 +385,8 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
setApiStatus(status);
|
||||
}
|
||||
|
||||
//! Construct the GET request to update all menu items.
|
||||
//
|
||||
function updateMenuItems() as Void {
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
||||
@ -365,17 +402,17 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
mTemplates = {};
|
||||
for (var i = 0; i < mItemsToUpdate.size(); i++) {
|
||||
var item = mItemsToUpdate[i];
|
||||
var template = item.buildTemplate();
|
||||
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).buildToggleTemplate()
|
||||
});
|
||||
}
|
||||
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
|
||||
@ -399,10 +436,16 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the API status.
|
||||
//! Callback function after completing the GET request to fetch the API status.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
(:glance)
|
||||
function onReturnFetchApiStatus(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
function onReturnFetchApiStatus(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String
|
||||
) as Void {
|
||||
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Data: " + data);
|
||||
|
||||
@ -463,8 +506,11 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Construct the GET request to test the API status, is it accessible?
|
||||
//
|
||||
(:glance)
|
||||
function fetchApiStatus() as Void {
|
||||
// System.println("API URL = " + Settings.getApiUrl());
|
||||
if (Settings.getApiUrl().equals("")) {
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||
WatchUi.requestUpdate();
|
||||
@ -502,30 +548,49 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
//! Record the API status result.
|
||||
//!
|
||||
//! @param s A string describing the API status
|
||||
//
|
||||
function setApiStatus(s as Lang.String) {
|
||||
mApiStatus = s;
|
||||
}
|
||||
|
||||
//! Return the API status result.
|
||||
//!
|
||||
//! @return A string describing the API status
|
||||
//
|
||||
(:glance)
|
||||
function getApiStatus() as Lang.String {
|
||||
return mApiStatus;
|
||||
}
|
||||
|
||||
//! Return the Menu status result.
|
||||
//!
|
||||
//! @return A string describing the Menu status
|
||||
//
|
||||
(:glance)
|
||||
function getMenuStatus() as Lang.String {
|
||||
return mMenuStatus;
|
||||
}
|
||||
|
||||
//! Return the Menu construction status.
|
||||
//!
|
||||
//! @return A Boolean indicating if the menu is loaded into the application.
|
||||
//
|
||||
function isHomeAssistantMenuLoaded() as Lang.Boolean {
|
||||
return mHaMenu != null;
|
||||
}
|
||||
|
||||
//! Make the menu visible on the watch face.
|
||||
//
|
||||
function pushHomeAssistantMenuView() as Void {
|
||||
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
|
||||
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take
|
||||
// alternative action if the test fails.
|
||||
//! Force status updates. Only take action if `Settings.getPollDelay() > 0`. This must be tested
|
||||
//! locally as it is then efficient to take alternative action if the test fails.
|
||||
//
|
||||
function forceStatusUpdates() as Void {
|
||||
// Don't mess with updates unless we are using a timer.
|
||||
if (Settings.getPollDelay() > 0) {
|
||||
@ -535,45 +600,61 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
//! Return the timer used to quit the application.
|
||||
//!
|
||||
//! @return Timer object
|
||||
//
|
||||
function getQuitTimer() as QuitTimer {
|
||||
return mQuitTimer;
|
||||
}
|
||||
|
||||
//! Return the glance view.
|
||||
//!
|
||||
//! @return The glance view
|
||||
//
|
||||
function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
|
||||
mIsGlance = true;
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
updateStatus();
|
||||
Settings.update();
|
||||
updateStatus();
|
||||
mGlanceTimer = new Timer.Timer();
|
||||
mGlanceTimer.start(method(:updateStatus), Globals.scApiBackoff, true);
|
||||
return [new HomeAssistantGlanceView(self)];
|
||||
}
|
||||
|
||||
// Required for the Glance update timer.
|
||||
//! Update the menu and API statuses. Required for the Glance update timer.
|
||||
//
|
||||
function updateStatus() as Void {
|
||||
mGlanceTimer = null;
|
||||
fetchMenuConfig();
|
||||
fetchApiStatus();
|
||||
}
|
||||
|
||||
//! Code for when the application settings are updated.
|
||||
//
|
||||
function onSettingsChanged() as Void {
|
||||
// System.println("HomeAssistantApp onSettingsChanged()");
|
||||
Settings.update();
|
||||
}
|
||||
|
||||
// Called each time the Registered Temporal Event is to be invoked. So the object is created each time on request and
|
||||
// then destroyed on completion (to save resources).
|
||||
//! Called each time the Registered Temporal Event is to be invoked. So the object is created each time
|
||||
//! on request and then destroyed on completion (to save resources).
|
||||
//
|
||||
function getServiceDelegate() as [ System.ServiceDelegate ] {
|
||||
return [new BackgroundServiceDelegate()];
|
||||
}
|
||||
|
||||
//! Determine is we are a glance or the full application. Glances should be considered to be separate applications.
|
||||
//
|
||||
function getIsApp() as Lang.Boolean {
|
||||
return mIsApp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//! Global function to return the application object.
|
||||
//
|
||||
(:glance, :background)
|
||||
function getApp() as HomeAssistantApp {
|
||||
return Application.getApp() as HomeAssistantApp;
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Calling a Home Assistant confirmation dialogue view.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -25,19 +20,27 @@ using Toybox.WatchUi;
|
||||
using Toybox.Timer;
|
||||
using Toybox.Application.Properties;
|
||||
|
||||
//! Calling a Home Assistant confirmation dialogue view.
|
||||
//
|
||||
class HomeAssistantConfirmation extends WatchUi.Confirmation {
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//! Delegate to respond to the confirmation request.
|
||||
//
|
||||
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
|
||||
private var mTimer as Timer.Timer or Null;
|
||||
private var mState as Lang.Boolean;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) {
|
||||
WatchUi.ConfirmationDelegate.initialize();
|
||||
mConfirmMethod = callback;
|
||||
@ -49,7 +52,12 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
function onResponse(response) as Lang.Boolean {
|
||||
//! Respond to the confirmation event.
|
||||
//!
|
||||
//! @param response code
|
||||
//! @return Required to meet the function prototype, but the base class does not indicate a definition.
|
||||
//
|
||||
function onResponse(response as WatchUi.Confirm) as Lang.Boolean {
|
||||
getApp().getQuitTimer().reset();
|
||||
if (mTimer != null) {
|
||||
mTimer.stop();
|
||||
@ -60,6 +68,7 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||
return true;
|
||||
}
|
||||
|
||||
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
||||
function onTimeout() as Void {
|
||||
mTimer.stop();
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
|
@ -11,17 +11,14 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 23 November 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Glance view for GarminHomeAssistant
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
//! Glance view for GarminHomeAssistant
|
||||
//
|
||||
(:glance)
|
||||
class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
private static const scLeftMargin = 5; // in pixels
|
||||
@ -34,6 +31,8 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
private var mMenuStatus as WatchUi.Text or Null;
|
||||
private var mAntiAlias as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize(app as HomeAssistantApp) {
|
||||
GlanceView.initialize();
|
||||
mApp = app;
|
||||
@ -42,6 +41,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
}
|
||||
}
|
||||
|
||||
//! Construct the view.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onLayout(dc as Graphics.Dc) as Void {
|
||||
var h = dc.getHeight();
|
||||
var tw = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String, Graphics.FONT_XTINY);
|
||||
@ -89,6 +92,10 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
});
|
||||
}
|
||||
|
||||
//! Update the view with the latest status text.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onUpdate(dc as Graphics.Dc) as Void {
|
||||
GlanceView.onUpdate(dc);
|
||||
if(mAntiAlias) {
|
||||
|
@ -11,21 +11,19 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
|
||||
// a Home Assistant Template.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
|
||||
private var mTemplate as Lang.String or Null;
|
||||
//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
|
||||
//! a Home Assistant Template.
|
||||
//
|
||||
class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
|
||||
private var mMenu as HomeAssistantView;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize(
|
||||
definition as Lang.Dictionary,
|
||||
template as Lang.String,
|
||||
@ -34,48 +32,25 @@ class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
|
||||
:alignment as WatchUi.MenuItem.Alignment
|
||||
} or Null
|
||||
) {
|
||||
if (options != null) {
|
||||
options.put(:icon, icon);
|
||||
} else {
|
||||
options = { :icon => icon };
|
||||
}
|
||||
|
||||
WatchUi.IconMenuItem.initialize(
|
||||
HomeAssistantMenuItem.initialize(
|
||||
definition.get("name") as Lang.String,
|
||||
null,
|
||||
null,
|
||||
icon,
|
||||
template,
|
||||
options
|
||||
);
|
||||
|
||||
mTemplate = template;
|
||||
mMenu = new HomeAssistantView(definition, null);
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel(null);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantGroupMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Return the submenu for this group menu item.
|
||||
//
|
||||
function getMenuView() as HomeAssistantView {
|
||||
return mMenu;
|
||||
}
|
||||
|
||||
function hasTemplate() as Lang.Boolean {
|
||||
return mTemplate != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
95
source/HomeAssistantMenuItem.mc
Normal file
@ -0,0 +1,95 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Distributed under MIT Licence
|
||||
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
//! Generic menu button with an icon that optionally renders a Home Assistant Template.
|
||||
//
|
||||
class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
|
||||
private var mTemplate as Lang.String or Null;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param label Menu item label
|
||||
//! @param template Menu item template
|
||||
//! @param options Menu item options to be passed on.
|
||||
//
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
|
||||
} or Null
|
||||
) {
|
||||
WatchUi.IconMenuItem.initialize(
|
||||
label,
|
||||
null,
|
||||
null,
|
||||
options.get(:icon),
|
||||
options
|
||||
);
|
||||
mTemplate = template;
|
||||
}
|
||||
|
||||
//! Does this menu item use a template?
|
||||
//!
|
||||
//! @return True if the menu has a defined template else false.
|
||||
//
|
||||
function hasTemplate() as Lang.Boolean {
|
||||
return mTemplate != null;
|
||||
}
|
||||
|
||||
//! Return the menu item's template.
|
||||
//!
|
||||
//! @return A string with the menu item's template definition.
|
||||
//
|
||||
function getTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
|
||||
//! Update the menu item's sub label to display the template rendered by Home Assistant.
|
||||
//!
|
||||
//! @param data The rendered template (typically a string) to be placed in the sub label. This may
|
||||
//! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such.
|
||||
//
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel($.Rez.Strings.Empty);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Number) {
|
||||
var d = data as Lang.Number;
|
||||
setSubLabel(d.format("%d"));
|
||||
} else if(data instanceof Lang.Float) {
|
||||
var f = data as Lang.Float;
|
||||
setSubLabel(f.format("%f"));
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAssistantMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, Number or Float, or the item cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
}
|
@ -11,17 +11,14 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 17 November 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// MenuItems Factory.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
//! MenuItems Factory class.
|
||||
//
|
||||
class HomeAssistantMenuItemFactory {
|
||||
private var mMenuItemOptions as Lang.Dictionary;
|
||||
private var mTapTypeIcon as WatchUi.Bitmap;
|
||||
@ -31,6 +28,8 @@ class HomeAssistantMenuItemFactory {
|
||||
|
||||
private static var instance;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
private function initialize() {
|
||||
mMenuItemOptions = {
|
||||
:alignment => Settings.getMenuAlignment()
|
||||
@ -57,6 +56,8 @@ class HomeAssistantMenuItemFactory {
|
||||
mHomeAssistantService = new HomeAssistantService();
|
||||
}
|
||||
|
||||
//! Create the one and only instance of this class.
|
||||
//
|
||||
static function create() as HomeAssistantMenuItemFactory {
|
||||
if (instance == null) {
|
||||
instance = new HomeAssistantMenuItemFactory();
|
||||
@ -64,6 +65,14 @@ class HomeAssistantMenuItemFactory {
|
||||
return instance;
|
||||
}
|
||||
|
||||
//! Toggle menu item.
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param entity_id Home Assistant Entity ID (optional)
|
||||
//! @param template Template for Home Assistant to render (optional)
|
||||
//! @param confirm Should this menu item selection be confirmed?
|
||||
//! @param pin Should this menu item selection request the security PIN?
|
||||
//
|
||||
function toggle(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity_id as Lang.String or Null,
|
||||
@ -81,20 +90,30 @@ class HomeAssistantMenuItemFactory {
|
||||
);
|
||||
}
|
||||
|
||||
//! Tap menu item.
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param entity_id Home Assistant Entity ID (optional)
|
||||
//! @param template Template for Home Assistant to render (optional)
|
||||
//! @param service Template for Home Assistant to render (optional)
|
||||
//! @param confirm Should this menu item selection be confirmed?
|
||||
//! @param pin Should this menu item selection request the security PIN?
|
||||
//! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
|
||||
//
|
||||
function tap(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity as Lang.String or Null,
|
||||
template as Lang.String or Null,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
pin as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity_id as Lang.String or Null,
|
||||
template as Lang.String or Null,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
pin as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null
|
||||
) as WatchUi.MenuItem {
|
||||
if (entity != null) {
|
||||
if (entity_id != null) {
|
||||
if (data == null) {
|
||||
data = { "entity_id" => entity };
|
||||
data = { "entity_id" => entity_id };
|
||||
} else {
|
||||
data.put("entity_id", entity);
|
||||
data.put("entity_id", entity_id);
|
||||
}
|
||||
}
|
||||
if (service != null) {
|
||||
@ -124,6 +143,11 @@ class HomeAssistantMenuItemFactory {
|
||||
}
|
||||
}
|
||||
|
||||
//! Group menu item.
|
||||
//!
|
||||
//! @param definition Items array from the JSON that defines this sub menu.
|
||||
//! @param template Template for Home Assistant to render (optional)
|
||||
//
|
||||
function group(
|
||||
definition as Lang.Dictionary,
|
||||
template as Lang.String or Null
|
||||
|
@ -9,27 +9,30 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Pin Confirmation dialog and logic.
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
import Toybox.WatchUi;
|
||||
import Toybox.Timer;
|
||||
import Toybox.Attention;
|
||||
import Toybox.Time;
|
||||
using Toybox.Graphics;
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Timer;
|
||||
using Toybox.Attention;
|
||||
using Toybox.Time;
|
||||
|
||||
//! Pin digit used for number 0..9
|
||||
//
|
||||
class PinDigit extends WatchUi.Selectable {
|
||||
|
||||
private var mDigit as Number;
|
||||
private var mDigit as Lang.Number;
|
||||
|
||||
function initialize(digit as Number, stepX as Number, stepY as Number) {
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param digit The digit this instance of the class represents and to display.
|
||||
//! @param stepX Horizontal spacing.
|
||||
//! @param stepY Vertical spacing.
|
||||
//
|
||||
function initialize(digit as Lang.Number, stepX as Lang.Number, stepY as Lang.Number) {
|
||||
var marginX = stepX * 0.05; // 5% margin on all sides
|
||||
var marginY = stepY * 0.05;
|
||||
var x = (digit == 0) ? stepX : stepX * ((digit+2) % 3); // layout '0' in 2nd col, others ltr in 3 columns
|
||||
@ -63,24 +66,40 @@ class PinDigit extends WatchUi.Selectable {
|
||||
});
|
||||
|
||||
mDigit = digit;
|
||||
|
||||
}
|
||||
|
||||
function getDigit() as Number {
|
||||
//! Return the digit 0..9 represented by this button
|
||||
//
|
||||
function getDigit() as Lang.Number {
|
||||
return mDigit;
|
||||
}
|
||||
|
||||
//! Customised drawing of a PIN digit's button.
|
||||
//
|
||||
class PinDigitButton extends WatchUi.Drawable {
|
||||
private var mText as Number;
|
||||
private var mTouched as Boolean = false;
|
||||
private var mText as Lang.Number;
|
||||
private var mTouched as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.<br>
|
||||
//! {<br>
|
||||
//!   :label as Lang.Number, // The digit 0..9 to display<br>
|
||||
//!   :touched as Lang.Boolean, // Should the digit be filled to indicate it has been pressed?<br>
|
||||
//!   + those required by `Drawable.initialize()`<br>
|
||||
//! }
|
||||
//
|
||||
function initialize(options) {
|
||||
Drawable.initialize(options);
|
||||
mText = options.get(:label);
|
||||
mTouched = options.get(:touched);
|
||||
}
|
||||
|
||||
function draw(dc) {
|
||||
//! Draw the PIN digit button.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function draw(dc as Graphics.Dc) {
|
||||
if (mTouched) {
|
||||
dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE);
|
||||
} else {
|
||||
@ -98,17 +117,27 @@ class PinDigit extends WatchUi.Selectable {
|
||||
|
||||
}
|
||||
|
||||
|
||||
//! Pin Confirmation dialog and logic.
|
||||
//
|
||||
class HomeAssistantPinConfirmationView extends WatchUi.View {
|
||||
|
||||
static const MARGIN_X = 20; // margin on left & right side of screen (overall prettier and works better on round displays)
|
||||
//! Margin on left & right side of screen (overall prettier and works better on round displays)
|
||||
static const MARGIN_X = 20;
|
||||
//! Indicates how many digits have been entered so far.
|
||||
var mPinMask as Lang.String = "";
|
||||
|
||||
var mPinMask as String = "";
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
View.initialize();
|
||||
}
|
||||
|
||||
function onLayout(dc as Dc) as Void {
|
||||
//! Construct the view.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onLayout(dc as Graphics.Dc) as Void {
|
||||
var stepX = (dc.getWidth() - MARGIN_X * 2) / 3; // three columns
|
||||
var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry)
|
||||
var digits = [];
|
||||
@ -119,7 +148,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
|
||||
setLayout(digits);
|
||||
}
|
||||
|
||||
function onUpdate(dc as Dc) as Void {
|
||||
//! Update the view.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onUpdate(dc as Graphics.Dc) as Void {
|
||||
View.onUpdate(dc);
|
||||
if (mPinMask.length() != 0) {
|
||||
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
|
||||
@ -127,7 +160,11 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
|
||||
}
|
||||
}
|
||||
|
||||
function updatePinMask(length as Number) {
|
||||
//! Update the PIN mask displayed.
|
||||
//!
|
||||
//! @param length Number of `*` characters to use for the mask string.
|
||||
//
|
||||
function updatePinMask(length as Lang.Number) {
|
||||
mPinMask = "";
|
||||
for (var i=0; i<length; i++) {
|
||||
mPinMask += "*";
|
||||
@ -138,17 +175,31 @@ class HomeAssistantPinConfirmationView extends WatchUi.View {
|
||||
}
|
||||
|
||||
|
||||
//! Delegate for the HomeAssistantPinConfirmationView.
|
||||
//
|
||||
class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
|
||||
private var mPin as String;
|
||||
private var mEnteredPin as String;
|
||||
private var mPin as Lang.String;
|
||||
private var mEnteredPin as Lang.String;
|
||||
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
|
||||
private var mTimer as Timer.Timer or Null;
|
||||
private var mState as Lang.Boolean;
|
||||
private var mFailures as PinFailures;
|
||||
private var mView as HomeAssistantPinConfirmationView;
|
||||
|
||||
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean, pin as String, view as HomeAssistantPinConfirmationView) {
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param callback Method to call on confirmation.
|
||||
//! @param state Current state of a toggle button.
|
||||
//! @param pin PIN to be matched.
|
||||
//! @param view PIN confirmation view.
|
||||
//
|
||||
function initialize(
|
||||
callback as Method(state as Lang.Boolean) as Void,
|
||||
state as Lang.Boolean,
|
||||
pin as Lang.String,
|
||||
view as HomeAssistantPinConfirmationView
|
||||
) {
|
||||
BehaviorDelegate.initialize();
|
||||
mFailures = new PinFailures();
|
||||
if (mFailures.isLocked()) {
|
||||
@ -165,7 +216,12 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
resetTimer();
|
||||
}
|
||||
|
||||
function onSelectable(event as SelectableEvent) as Boolean {
|
||||
//! Add another entered digit to the "PIN so far". When it is long enough verify the PIN is correct and the
|
||||
//! invoke the supplied call back function.
|
||||
//!
|
||||
//! @param event The digit pressed by the user tapping the screen.
|
||||
//
|
||||
function onSelectable(event as WatchUi.SelectableEvent) as Lang.Boolean {
|
||||
if (mFailures.isLocked()) {
|
||||
goBack();
|
||||
}
|
||||
@ -173,7 +229,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) {
|
||||
mEnteredPin += instance.getDigit();
|
||||
createUserFeedback();
|
||||
// System.println("HomeAssitantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
|
||||
// System.println("HomeAssistantPinConfirmationDelegate onSelectable() mEnteredPin = " + mEnteredPin);
|
||||
if (mEnteredPin.length() == mPin.length()) {
|
||||
if (mEnteredPin.equals(mPin)) {
|
||||
mFailures.reset();
|
||||
@ -193,6 +249,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
return true;
|
||||
}
|
||||
|
||||
//! Hepatic feedback.
|
||||
//
|
||||
function createUserFeedback() {
|
||||
if (Attention has :vibrate && Settings.getVibrate()) {
|
||||
Attention.vibrate([new Attention.VibeProfile(25, 25)]);
|
||||
@ -200,6 +258,9 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
mView.updatePinMask(mEnteredPin.length());
|
||||
}
|
||||
|
||||
//! A timer is used to clear the PIN entry view if digits are not pressed. So each time a digit is pressed the
|
||||
//! timer is reset.
|
||||
//
|
||||
function resetTimer() {
|
||||
var timeout = Settings.getConfirmTimeout(); // ms
|
||||
if (timeout > 0) {
|
||||
@ -212,6 +273,8 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
//! Cancel PIN entry.
|
||||
//
|
||||
function goBack() as Void {
|
||||
if (mTimer != null) {
|
||||
mTimer.stop();
|
||||
@ -219,18 +282,20 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
}
|
||||
|
||||
//! Hepatic feedback for a wrong PIN and cancel entry.
|
||||
//
|
||||
function error() as Void {
|
||||
// System.println("HomeAssistantPinConfirmationDelegate error() Wrong PIN entered");
|
||||
mFailures.addFailure();
|
||||
if (Attention has :vibrate && Settings.getVibrate()) {
|
||||
Attention.vibrate([
|
||||
new Attention.VibeProfile(100, 100),
|
||||
new Attention.VibeProfile(0, 200),
|
||||
new Attention.VibeProfile(75, 100),
|
||||
new Attention.VibeProfile(0, 200),
|
||||
new Attention.VibeProfile(50, 100),
|
||||
new Attention.VibeProfile(0, 200),
|
||||
new Attention.VibeProfile(25, 100)
|
||||
new Attention.VibeProfile( 0, 200),
|
||||
new Attention.VibeProfile( 75, 100),
|
||||
new Attention.VibeProfile( 0, 200),
|
||||
new Attention.VibeProfile( 50, 100),
|
||||
new Attention.VibeProfile( 0, 200),
|
||||
new Attention.VibeProfile( 25, 100)
|
||||
]);
|
||||
}
|
||||
if (WatchUi has :showToast) {
|
||||
@ -241,14 +306,19 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
||||
|
||||
}
|
||||
|
||||
|
||||
//! Manage PIN entry failures to try and prevent brute force exhaustion by inserting delays in retries.
|
||||
//
|
||||
class PinFailures {
|
||||
|
||||
const STORAGE_KEY_FAILURES as String = "pin_failures";
|
||||
const STORAGE_KEY_LOCKED as String = "pin_locked";
|
||||
const STORAGE_KEY_FAILURES as Lang.String = "pin_failures";
|
||||
const STORAGE_KEY_LOCKED as Lang.String = "pin_locked";
|
||||
|
||||
private var mFailures as Array<Number>;
|
||||
private var mLockedUntil as Number or Null;
|
||||
private var mFailures as Lang.Array<Lang.Number>;
|
||||
private var mLockedUntil as Lang.Number or Null;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
// System.println("PinFailures initialize() Initializing PIN failures from storage");
|
||||
var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES);
|
||||
@ -256,6 +326,8 @@ class PinFailures {
|
||||
mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED);
|
||||
}
|
||||
|
||||
//! Record a PIN entry failure. If too many have occurred lock the application.
|
||||
//
|
||||
function addFailure() {
|
||||
mFailures.add(Time.now().value());
|
||||
// System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded");
|
||||
@ -268,7 +340,7 @@ class PinFailures {
|
||||
mFailures = mFailures.slice(1, null);
|
||||
} else {
|
||||
mFailures = [];
|
||||
mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Gregorian.SECONDS_PER_MINUTE)).value();
|
||||
mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Time.Gregorian.SECONDS_PER_MINUTE)).value();
|
||||
Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil);
|
||||
// System.println("PinFailures addFailure() Locked until " + mLockedUntil);
|
||||
}
|
||||
@ -276,6 +348,9 @@ class PinFailures {
|
||||
Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures);
|
||||
}
|
||||
|
||||
//! Clear the record of previous PIN entry failures, e.g. because the correct PIN has now been entered
|
||||
//! within tolerance.
|
||||
//
|
||||
function reset() {
|
||||
// System.println("PinFailures reset() Resetting failures");
|
||||
mFailures = [];
|
||||
@ -284,11 +359,18 @@ class PinFailures {
|
||||
Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
|
||||
}
|
||||
|
||||
function getLockedUntilSeconds() as Number {
|
||||
//! Retrieve the remaining time the application must be locked out for.
|
||||
//
|
||||
function getLockedUntilSeconds() as Lang.Number {
|
||||
return new Time.Moment(mLockedUntil).subtract(Time.now()).value();
|
||||
}
|
||||
|
||||
function isLocked() as Boolean {
|
||||
//! Is the application currently locked out? If the application is no longer locked out, then clear the
|
||||
//! stored values used to determine this state.
|
||||
//!
|
||||
//! @return Boolean indicating if the application is currently locked out.
|
||||
//
|
||||
function isLocked() as Lang.Boolean {
|
||||
if (mLockedUntil == null) {
|
||||
return false;
|
||||
}
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 19 November 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Calling a Home Assistant Service.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -23,10 +18,14 @@ using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
using Toybox.Application.Properties;
|
||||
|
||||
//! Calling a Home Assistant Service.
|
||||
//
|
||||
class HomeAssistantService {
|
||||
private var mHasToast as Lang.Boolean = false;
|
||||
private var mHasVibrate as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
if (WatchUi has :showToast) {
|
||||
mHasToast = true;
|
||||
@ -36,7 +35,11 @@ class HomeAssistantService {
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the POST request to call a service.
|
||||
//! Callback function after completing the POST request to call a service.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//! @param context An `entity_id` supplied in the GET request `options` `Lang.Dictionary` `context` field.
|
||||
//
|
||||
function onReturnCall(
|
||||
responseCode as Lang.Number,
|
||||
@ -107,6 +110,11 @@ class HomeAssistantService {
|
||||
}
|
||||
}
|
||||
|
||||
//! Invoke a service call for a menu item.
|
||||
//!
|
||||
//! @param service The Home Assistant service to be run, e.g. from the JSON `service` field.
|
||||
//! @param data Data to be supplied to the service call.
|
||||
//
|
||||
function call(
|
||||
service as Lang.String,
|
||||
data as Lang.Dictionary or Null
|
||||
|
@ -9,12 +9,7 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Menu button that triggers a service.
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -22,14 +17,28 @@ using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
//! Menu button that triggers a service.
|
||||
//
|
||||
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
private var mHomeAssistantService as HomeAssistantService;
|
||||
private var mTemplate as Lang.String;
|
||||
private var mService as Lang.String or Null;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mPin as Lang.Boolean;
|
||||
private var mData as Lang.Dictionary or Null;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param template Menu item template.
|
||||
//! @param service Menu item service.
|
||||
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
|
||||
//! @param pin Should the service call be protected with a PIN for some low level of security?
|
||||
//! @param data Data to supply to the service call.
|
||||
//! @param icon Icon to use for the menu item.
|
||||
//! @param options Menu item options to be passed on.
|
||||
//! @param haService Shared Home Assistant service object that will perform the required call. Only
|
||||
//! one of these objects is created for all menu items to re-use.
|
||||
//
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
@ -39,53 +48,32 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
data as Lang.Dictionary or Null,
|
||||
icon as Graphics.BitmapType or WatchUi.Drawable,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
|
||||
} or Null,
|
||||
haService as HomeAssistantService
|
||||
) {
|
||||
WatchUi.IconMenuItem.initialize(
|
||||
if (options != null) {
|
||||
options.put(:icon, icon);
|
||||
} else {
|
||||
options = { :icon => icon };
|
||||
}
|
||||
|
||||
HomeAssistantMenuItem.initialize(
|
||||
label,
|
||||
null,
|
||||
null,
|
||||
icon,
|
||||
template,
|
||||
options
|
||||
);
|
||||
|
||||
mHomeAssistantService = haService;
|
||||
mTemplate = template;
|
||||
mService = service;
|
||||
mConfirm = confirm;
|
||||
mPin = pin;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
function hasTemplate() as Lang.Boolean {
|
||||
return mTemplate != null;
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel($.Rez.Strings.Empty);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
|
||||
//
|
||||
function callService() as Void {
|
||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||
if (mPin && hasTouchScreen) {
|
||||
@ -109,7 +97,10 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
// NB. Parameter 'b' is ignored
|
||||
//! Callback function after the menu items selection has been (optionally) confirmed.
|
||||
//!
|
||||
//! @param b Ignored. It is included in order to match the expected function prototype of the callback method.
|
||||
//
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
if (mService != null) {
|
||||
mHomeAssistantService.call(mService, mData);
|
||||
|
@ -9,12 +9,7 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Light or switch toggle button that calls the API to maintain the up to date state.
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -24,6 +19,8 @@ using Toybox.Graphics;
|
||||
using Toybox.Application.Properties;
|
||||
using Toybox.Timer;
|
||||
|
||||
//! Light or switch toggle menu button that calls the API to maintain the up to date state.
|
||||
//
|
||||
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mPin as Lang.Boolean;
|
||||
@ -31,6 +28,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
private var mTemplate as Lang.String;
|
||||
private var mHasVibrate as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param template Menu item template.
|
||||
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
|
||||
//! @param pin Should the service call be protected with a PIN for some low level of security?
|
||||
//! @param data Data to supply to the service call.
|
||||
//! @param options Menu item options to be passed on.
|
||||
//
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
@ -42,7 +48,13 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
|
||||
} or Null
|
||||
) {
|
||||
WatchUi.ToggleMenuItem.initialize(label, null, null, false, options);
|
||||
WatchUi.ToggleMenuItem.initialize(
|
||||
label,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
options
|
||||
);
|
||||
if (Attention has :vibrate) {
|
||||
mHasVibrate = true;
|
||||
}
|
||||
@ -52,6 +64,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
mTemplate = template;
|
||||
}
|
||||
|
||||
//! Set the state of a toggle menu item.
|
||||
//
|
||||
private function setUiToggle(state as Null or Lang.String) as Void {
|
||||
if (state != null) {
|
||||
if (state.equals("on") && !isEnabled()) {
|
||||
@ -62,31 +76,55 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
//! Return the menu item's template.
|
||||
//!
|
||||
//! @return A string with the menu item's template definition.
|
||||
//
|
||||
function getTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
function buildToggleTemplate() as Lang.String or Null {
|
||||
|
||||
//! Return a toggle menu item's state template.
|
||||
//!
|
||||
//! @return A string with the menu item's template definition.
|
||||
//
|
||||
function getToggleTemplate() as Lang.String or Null {
|
||||
return "{{states('" + mData.get("entity_id") + "')}}";
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
//! Update the menu item's label from a recent GET request.
|
||||
//!
|
||||
//! @param data This should be a string, but the way the GET response is parsed, it can also be a number.
|
||||
//
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel(null);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Number) {
|
||||
var d = data as Lang.Number;
|
||||
setSubLabel(d.format("%d"));
|
||||
} else if(data instanceof Lang.Float) {
|
||||
var f = data as Lang.Float;
|
||||
setSubLabel(f.format("%f"));
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
|
||||
// System.println("HomeAssistantToggleMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
// The template must return a Lang.String, Number or Float, or the item cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Update the menu item's toggle state from a recent GET request.
|
||||
//!
|
||||
//! @param data This should be a string of either "on" or "off".
|
||||
//
|
||||
function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setUiToggle("off");
|
||||
@ -96,7 +134,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
setSubLabel($.Rez.Strings.Unavailable);
|
||||
}
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
|
||||
// System.println("HomeAssistantToggleMenuItem updateState() data = " + data);
|
||||
if (mTemplate == null) {
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
@ -113,9 +151,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
// Callback function after completing the POST request to set the status.
|
||||
//! Callback function after completing the POST request to set the status.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
function onReturnSetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
function onReturnSetState(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String
|
||||
) as Void {
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState() Response Data: " + data);
|
||||
|
||||
@ -170,6 +214,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
getApp().setApiStatus(status);
|
||||
}
|
||||
|
||||
//! Set the state of the toggle menu item.
|
||||
//!
|
||||
//! @param s Boolean indicating the desired state of the toggle switch.
|
||||
//
|
||||
function setState(s as Lang.Boolean) as Void {
|
||||
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
|
||||
setEnabled(!isEnabled());
|
||||
@ -215,6 +263,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
|
||||
//
|
||||
function callService(b as Lang.Boolean) as Void {
|
||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||
if (mPin && hasTouchScreen) {
|
||||
@ -238,6 +288,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
//! Callback function to toggle state of this item after (optional) confirmation.
|
||||
//!
|
||||
//! @param b Desired toggle button state.
|
||||
//
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
setState(b);
|
||||
}
|
||||
|
@ -9,12 +9,7 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant menu construction.
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -24,8 +19,12 @@ using Toybox.Graphics;
|
||||
using Toybox.System;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
//! Home Assistant menu construction.
|
||||
//
|
||||
class HomeAssistantView extends WatchUi.Menu2 {
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize(
|
||||
definition as Lang.Dictionary,
|
||||
options as {
|
||||
@ -64,7 +63,8 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
if (type != null && name != null) {
|
||||
if (type.equals("toggle") && entity != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm, pin));
|
||||
} else if ((type.equals("tap") && service != null) || (type.equals("template") && content != null)) {
|
||||
} else if ((type.equals("tap") && service != null) || (type.equals("info") && content != null) || (type.equals("template") && content != null)) {
|
||||
// NB. "template" is deprecated in the schema and remains only for backward compatibility. All menu items can now use templates, so the replacement is "info".
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, pin, data));
|
||||
} else if (type.equals("group")) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
|
||||
@ -74,7 +74,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
}
|
||||
}
|
||||
|
||||
// Lang.Array.addAll() fails structural type checking without including "Null" in the return type
|
||||
//! Return a list of items that need to be updated within this menu structure.
|
||||
//!
|
||||
//! MN. Lang.Array.addAll() fails structural type checking without including "Null" in the return type
|
||||
//!
|
||||
//! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state.
|
||||
//
|
||||
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> {
|
||||
var fullList = [];
|
||||
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
|
||||
@ -101,26 +106,35 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
return fullList;
|
||||
}
|
||||
|
||||
// Called when this View is brought to the foreground. Restore
|
||||
// the state of this View and prepare it to be shown. This includes
|
||||
// loading resources into memory.
|
||||
//! Called when this View is brought to the foreground. Restore
|
||||
//! the state of this View and prepare it to be shown. This includes
|
||||
//! loading resources into memory.
|
||||
function onShow() as Void {}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
|
||||
//! Delegate for the HomeAssistantView.
|
||||
//!
|
||||
//! Reference: https://developer.garmin.com/connect-iq/core-topics/input-handling/
|
||||
//
|
||||
class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
|
||||
private var mIsRootMenuView as Lang.Boolean = false;
|
||||
private var mTimer as QuitTimer;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param isRootMenuView As menus can be nested, this state marks the top level menu so that the
|
||||
//! back event can exit the application completely rather than just popping
|
||||
//! a menu view.
|
||||
//
|
||||
function initialize(isRootMenuView as Lang.Boolean) {
|
||||
Menu2InputDelegate.initialize();
|
||||
mIsRootMenuView = isRootMenuView;
|
||||
mTimer = getApp().getQuitTimer();
|
||||
}
|
||||
|
||||
//! Back button event
|
||||
//
|
||||
function onBack() {
|
||||
mTimer.reset();
|
||||
|
||||
@ -134,16 +148,22 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
}
|
||||
|
||||
// Only for CheckboxMenu
|
||||
//! Only for CheckboxMenu
|
||||
//
|
||||
function onDone() {
|
||||
mTimer.reset();
|
||||
}
|
||||
|
||||
// Only for CustomMenu
|
||||
//! Only for CustomMenu
|
||||
//
|
||||
function onFooter() {
|
||||
mTimer.reset();
|
||||
}
|
||||
|
||||
//! Select event
|
||||
//!
|
||||
//! @param item Selected menu item.
|
||||
//
|
||||
function onSelect(item as WatchUi.MenuItem) as Void {
|
||||
mTimer.reset();
|
||||
if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
@ -163,7 +183,8 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// Only for CustomMenu
|
||||
//! Only for CustomMenu
|
||||
//
|
||||
function onTitle() {
|
||||
mTimer.reset();
|
||||
}
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Quit the application after a period of inactivity in order to save the battery.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -23,18 +18,27 @@ using Toybox.Timer;
|
||||
using Toybox.Application.Properties;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
//! Quit the application after a period of inactivity in order to save the battery.
|
||||
//!
|
||||
class QuitTimer extends Timer.Timer {
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
Timer.Timer.initialize();
|
||||
}
|
||||
|
||||
//! Can't see how to make a method object from `System.exit()` without this layer of
|
||||
//! indirection. I assume this is because `System` is a static class.
|
||||
//
|
||||
function exitApp() as Void {
|
||||
// System.println("QuitTimer exitApp(): Exiting");
|
||||
// This will exit the system cleanly from any point within an app.
|
||||
System.exit();
|
||||
}
|
||||
|
||||
//! Kick off the quit timer.
|
||||
//
|
||||
function begin() {
|
||||
var api_timeout = Settings.getAppTimeout(); // ms
|
||||
if (api_timeout > 0) {
|
||||
@ -42,6 +46,8 @@ class QuitTimer extends Timer.Timer {
|
||||
}
|
||||
}
|
||||
|
||||
//! Reset the quit timer.
|
||||
//
|
||||
function reset() {
|
||||
// System.println("QuitTimer reset(): Restarted quit timer");
|
||||
stop();
|
||||
|
@ -11,35 +11,36 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// A view with added methods to scale from percentages of scrren size to pixels.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Math;
|
||||
|
||||
//! A view that provides a common method 'pixelsForScreen' to make Views easier to layout on different
|
||||
//! sized watch screens.
|
||||
//
|
||||
class ScalableView extends WatchUi.View {
|
||||
//! Retain the local screen width for efficiency
|
||||
private var mScreenWidth;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
View.initialize();
|
||||
mScreenWidth = System.getDeviceSettings().screenWidth;
|
||||
}
|
||||
|
||||
// Convert a fraction expressed as a percentage (%) to a number of pixels for the
|
||||
// screen's dimensions.
|
||||
//
|
||||
// Parameters:
|
||||
// * dc - Device context
|
||||
// * pc - Percentage (%) expressed as a number in the range 0.0..100.0
|
||||
//
|
||||
// Uses screen width rather than screen height as rectangular screens tend to have
|
||||
// height > width.
|
||||
//
|
||||
//! Convert a fraction expressed as a percentage (%) to a number of pixels for the
|
||||
//! screen's dimensions.
|
||||
//!
|
||||
//! Uses screen width rather than screen height as rectangular screens tend to have
|
||||
//! height > width.
|
||||
//!
|
||||
//! @param pc Percentage (%) expressed as a number in the range 0.0..100.0
|
||||
//!
|
||||
//! @return Number of pixels for the screen's dimensions for a fraction expressed as a percentage (%).
|
||||
//!
|
||||
function pixelsForScreen(pc as Lang.Float) as Lang.Number {
|
||||
return Math.round(pc * mScreenWidth) / 100;
|
||||
}
|
||||
|
@ -9,17 +9,7 @@
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey, SomeoneOnEarth, 23 November 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant settings.
|
||||
//
|
||||
// WARNING!
|
||||
//
|
||||
// Careful putting ErrorView.show() calls in here. They need to be guarded so that
|
||||
// they do not get called when only displaying the glance view.
|
||||
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 23 November 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -31,6 +21,11 @@ using Toybox.System;
|
||||
using Toybox.Background;
|
||||
using Toybox.Time;
|
||||
|
||||
//! Home Assistant settings.
|
||||
//!
|
||||
//! <em>WARNING!</em> Careful putting ErrorView.show() calls in here. They need to be
|
||||
//! guarded so that they do not get called when only displaying the glance view.
|
||||
//
|
||||
(:glance, :background)
|
||||
class Settings {
|
||||
private static var mApiKey as Lang.String = "";
|
||||
@ -40,19 +35,24 @@ class Settings {
|
||||
private static var mCacheConfig as Lang.Boolean = false;
|
||||
private static var mClearCache as Lang.Boolean = false;
|
||||
private static var mVibrate as Lang.Boolean = false;
|
||||
private static var mAppTimeout as Lang.Number = 0; // seconds
|
||||
private static var mPollDelay as Lang.Number = 0; // seconds
|
||||
private static var mConfirmTimeout as Lang.Number = 3; // seconds
|
||||
//! seconds
|
||||
private static var mAppTimeout as Lang.Number = 0;
|
||||
//! seconds
|
||||
private static var mPollDelay as Lang.Number = 0;
|
||||
//! seconds
|
||||
private static var mConfirmTimeout as Lang.Number = 3;
|
||||
private static var mPin as Lang.String or Null = "0000";
|
||||
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
|
||||
private static var mIsSensorsLevelEnabled as Lang.Boolean = false;
|
||||
private static var mBatteryRefreshRate as Lang.Number = 15; // minutes
|
||||
//! minutes
|
||||
private static var mBatteryRefreshRate as Lang.Number = 15;
|
||||
private static var mIsApp as Lang.Boolean = false;
|
||||
private static var mHasService as Lang.Boolean = false;
|
||||
// Must keep the object so it doesn't get garbage collected.
|
||||
//! Must keep the object so it doesn't get garbage collected.
|
||||
private static var mWebhookManager as WebhookManager or Null;
|
||||
|
||||
// Called on application start and then whenever the settings are changed.
|
||||
//! Called on application start and then whenever the settings are changed.
|
||||
//
|
||||
static function update() {
|
||||
mIsApp = getApp().getIsApp();
|
||||
mApiKey = Properties.getValue("api_key");
|
||||
@ -69,7 +69,11 @@ class Settings {
|
||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
||||
}
|
||||
|
||||
//! A webhook is required for non-privileged API calls.
|
||||
//
|
||||
static function webhook() {
|
||||
if (System has :ServiceDelegate) {
|
||||
mHasService = true;
|
||||
}
|
||||
@ -106,73 +110,131 @@ class Settings {
|
||||
unsetWebhookId();
|
||||
}
|
||||
}
|
||||
// System.println("Settings update(): getTemporalEventRegisteredTime() = " + Background.getTemporalEventRegisteredTime());
|
||||
// System.println("Settings webhook(): getTemporalEventRegisteredTime() = " + Background.getTemporalEventRegisteredTime());
|
||||
// if (Background.getTemporalEventRegisteredTime() != null) {
|
||||
// System.println("Settings update(): getTemporalEventRegisteredTime().value() = " + Background.getTemporalEventRegisteredTime().value().format("%d") + " seconds");
|
||||
// System.println("Settings webhook(): getTemporalEventRegisteredTime().value() = " + Background.getTemporalEventRegisteredTime().value().format("%d") + " seconds");
|
||||
// } else {
|
||||
// System.println("Settings update(): getTemporalEventRegisteredTime() = null");
|
||||
// System.println("Settings webhook(): getTemporalEventRegisteredTime() = null");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
//! Get the API key supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The API Key
|
||||
//
|
||||
static function getApiKey() as Lang.String {
|
||||
return mApiKey;
|
||||
}
|
||||
|
||||
//! Get the Webhook ID supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The Webhook ID
|
||||
//
|
||||
static function getWebhookId() as Lang.String {
|
||||
return mWebhookId;
|
||||
}
|
||||
|
||||
//! Set the Webhook ID supplied as part of the Settings.
|
||||
//!
|
||||
//! @param webhookId The Webhook ID value to be saved.
|
||||
//
|
||||
static function setWebhookId(webhookId as Lang.String) {
|
||||
mWebhookId = webhookId;
|
||||
Properties.setValue("webhook_id", mWebhookId);
|
||||
}
|
||||
|
||||
//! Delete the Webhook ID saved as part of the Settings.
|
||||
//
|
||||
static function unsetWebhookId() {
|
||||
mWebhookId = "";
|
||||
Properties.setValue("webhook_id", mWebhookId);
|
||||
}
|
||||
|
||||
//! Get the API URL supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The API URL
|
||||
//
|
||||
static function getApiUrl() as Lang.String {
|
||||
return mApiUrl;
|
||||
}
|
||||
|
||||
//! Get the menu configuration URL supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The menu configuration URL
|
||||
//
|
||||
static function getConfigUrl() as Lang.String {
|
||||
return mConfigUrl;
|
||||
}
|
||||
|
||||
//! Get the menu cache Boolean option supplied as part of the Settings.
|
||||
//!
|
||||
//! @return Boolean for whether the menu should be cached to save application
|
||||
//! start up time.
|
||||
//
|
||||
static function getCacheConfig() as Lang.Boolean {
|
||||
return mCacheConfig;
|
||||
}
|
||||
|
||||
//! Get the clear cache Boolean option supplied as part of the Settings.
|
||||
//!
|
||||
//! @return Boolean for whether the cache should be cleared next time the
|
||||
//! application is started, forcing a menu refresh.
|
||||
//
|
||||
static function getClearCache() as Lang.Boolean {
|
||||
return mClearCache;
|
||||
}
|
||||
|
||||
//! Unset the clear cache Boolean option supplied as part of the Settings.
|
||||
//
|
||||
static function unsetClearCache() {
|
||||
mClearCache = false;
|
||||
Properties.setValue("clear_cache", mClearCache);
|
||||
}
|
||||
|
||||
//! Get the vibration Boolean option supplied as part of the Settings.
|
||||
//!
|
||||
//! @return Boolean for whether vibration is enabled.
|
||||
//
|
||||
static function getVibrate() as Lang.Boolean {
|
||||
return mVibrate;
|
||||
}
|
||||
|
||||
//! Get the application timeout value supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The application timeout in milliseconds.
|
||||
//
|
||||
static function getAppTimeout() as Lang.Number {
|
||||
return mAppTimeout * 1000; // Convert to milliseconds
|
||||
}
|
||||
|
||||
//! Get the application API polling interval supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The application API polling interval in milliseconds.
|
||||
//
|
||||
static function getPollDelay() as Lang.Number {
|
||||
return mPollDelay * 1000; // Convert to milliseconds
|
||||
}
|
||||
|
||||
//! Get the menu item confirmation delay supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The menu item confirmation delay in milliseconds.
|
||||
//
|
||||
static function getConfirmTimeout() as Lang.Number {
|
||||
return mConfirmTimeout * 1000; // Convert to milliseconds
|
||||
}
|
||||
|
||||
//! Get the menu item security PIN supplied as part of the Settings.
|
||||
//!
|
||||
//! @return The menu item security PIN.
|
||||
//
|
||||
static function getPin() as Lang.String or Null {
|
||||
return mPin;
|
||||
}
|
||||
|
||||
//! Check the user selected PIN confirms to 4 digits as a string.
|
||||
//!
|
||||
//! @return The validated 4 digit string.
|
||||
//
|
||||
private static function validatePin() as Lang.String or Null {
|
||||
var pin = Properties.getValue("pin");
|
||||
if (pin.toNumber() == null || pin.length() != 4) {
|
||||
@ -181,14 +243,24 @@ class Settings {
|
||||
return pin;
|
||||
}
|
||||
|
||||
//! Get the menu item alignment as part of the Settings.
|
||||
//!
|
||||
//! @return The menu item alignment.
|
||||
//
|
||||
static function getMenuAlignment() as Lang.Number {
|
||||
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
|
||||
}
|
||||
|
||||
//! Is logging of the watch sensors enabled? E.g. battery, activity etc.
|
||||
//!
|
||||
//! @return Boolean for whether logging of the watch sensors is enabled.
|
||||
//
|
||||
static function isSensorsLevelEnabled() as Lang.Boolean {
|
||||
return mIsSensorsLevelEnabled;
|
||||
}
|
||||
|
||||
//! Disable logging of the watch's sensors.
|
||||
//
|
||||
static function unsetIsSensorsLevelEnabled() {
|
||||
mIsSensorsLevelEnabled = false;
|
||||
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);
|
||||
|
123
source/WebLog.mc
@ -11,88 +11,99 @@
|
||||
//
|
||||
// J D Abbey & P A Abbey, 28 December 2022
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// WebLog provides a logging and hence debugging aid for when the application is
|
||||
// deployed to the watch. This is only used for development and use of it must not
|
||||
// persist into a deployed version. It uses a string buffer to group log entries into
|
||||
// larger submissions in order to prevent overflow of the blue tooth stack.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Usage:
|
||||
// wl = new WebLog();
|
||||
// wl.clear();
|
||||
// wl.println("Debug Message");
|
||||
// wl.flush();
|
||||
//
|
||||
// https://domain.name/path/log.php
|
||||
//
|
||||
// <?php
|
||||
// $myfile = fopen("log", "a");
|
||||
// $queries = array();
|
||||
// parse_str($_SERVER['QUERY_STRING'], $queries);
|
||||
// fwrite($myfile, $queries['log']);
|
||||
// print "Success";
|
||||
// ?>
|
||||
//
|
||||
// Logs published to: https://domain.name/path/log
|
||||
//
|
||||
// https://domain.name/path/log_clear.php
|
||||
//
|
||||
// <?php
|
||||
// $myfile = fopen("log", "w");
|
||||
// fwrite($myfile, "");
|
||||
// print "Success";
|
||||
// ?>
|
||||
|
||||
using Toybox.Communications;
|
||||
using Toybox.Lang;
|
||||
using Toybox.System;
|
||||
|
||||
//! WebLog provides a logging and hence debugging aid for when the application is
|
||||
//! deployed to the watch. This is only used for development and use of it must not
|
||||
//! persist into a deployed version. It uses a string buffer to group log entries into
|
||||
//! larger submissions in order to prevent overflow of the Bluetooth stack.
|
||||
//!
|
||||
//! Usage:
|
||||
//! <pre>
|
||||
//! wl = new WebLog();
|
||||
//! wl.clear();
|
||||
//! wl.println("Debug Message");
|
||||
//! wl.flush();
|
||||
//! </pre>
|
||||
//!
|
||||
//! File: https://domain.name/path/log.php
|
||||
//!
|
||||
//! <pre>
|
||||
//! <?php
|
||||
//! $myfile = fopen("log", "a");
|
||||
//! $queries = array();
|
||||
//! parse_str($_SERVER['QUERY_STRING'], $queries);
|
||||
//! fwrite($myfile, $queries['log']);
|
||||
//! print "Success";
|
||||
//! ?>
|
||||
//! </pre>
|
||||
//!
|
||||
//! Logs published to https://domain.name/path/log.
|
||||
//!
|
||||
//! File: https://domain.name/path/log_clear.php
|
||||
//!
|
||||
//! <pre>
|
||||
//! <?php
|
||||
//! $myfile = fopen("log", "w");
|
||||
//! fwrite($myfile, "");
|
||||
//! print "Success";
|
||||
//! ?>
|
||||
//! </pre>
|
||||
//
|
||||
(:glance, :background)
|
||||
class WebLog {
|
||||
private var callsbuffer = 4 as Lang.Number;
|
||||
private var callsBuffer = 4 as Lang.Number;
|
||||
private var numCalls = 0 as Lang.Number;
|
||||
private var buffer = "" as Lang.String;
|
||||
|
||||
// Set the number of calls to print() before sending the buffer to the online
|
||||
// logger.
|
||||
//! Set the number of calls to print() before sending the buffer to the online
|
||||
//! logger.
|
||||
//!
|
||||
//! @param l The number of log calls to buffer before writing to the online service.
|
||||
//
|
||||
function setCallsBuffer(l as Lang.Number) {
|
||||
callsbuffer = l;
|
||||
callsBuffer = l;
|
||||
}
|
||||
|
||||
// Get the number of calls to print() before sending the buffer to the online
|
||||
// logger.
|
||||
//! Get the number of calls to print() before sending the buffer to the online
|
||||
//! logger.
|
||||
//!
|
||||
//! @return The number of log calls to buffer before writing to the online service.
|
||||
//
|
||||
function getCallsBuffer() as Lang.Number {
|
||||
return callsbuffer;
|
||||
return callsBuffer;
|
||||
}
|
||||
|
||||
// Create a debug log over the Internet to keep track of the watch's runtime
|
||||
// execution.
|
||||
//! Create a debug log over the Internet to keep track of the watch's runtime
|
||||
//! execution.
|
||||
//!
|
||||
//! @param str The string to log.
|
||||
//
|
||||
function print(str as Lang.String) {
|
||||
var myTime = System.getClockTime();
|
||||
buffer += myTime.hour.format("%02d") + ":" + myTime.min.format("%02d") + ":" + myTime.sec.format("%02d") + " " + str;
|
||||
numCalls++;
|
||||
// System.println("WebLog print() str = " + str);
|
||||
if (numCalls >= callsbuffer) {
|
||||
if (numCalls >= callsBuffer) {
|
||||
doPrint();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a debug log over the Internet to keep track of the watch's runtime
|
||||
// execution. Add a new line character to the end.
|
||||
//! Create a debug log over the Internet to keep track of the watch's runtime
|
||||
//! execution. Add a new line character to the end.
|
||||
//!
|
||||
//! @param str The string to log.
|
||||
//
|
||||
function println(str as Lang.String) {
|
||||
print(str + "\n");
|
||||
}
|
||||
|
||||
// Flush the current buffer to the online logger even if it has not reach the
|
||||
// submission level set by 'callsbuffer'.
|
||||
//! Flush the current buffer to the online logger even if it has not reach the
|
||||
//! submission level set by 'callsBuffer'.
|
||||
//
|
||||
function flush() {
|
||||
// System.println("WebLog flush()");
|
||||
@ -101,7 +112,7 @@ class WebLog {
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the submission to the online logger.
|
||||
//! Perform the submission to the online logger.
|
||||
//
|
||||
function doPrint() {
|
||||
// System.println("WebLog doPrint()");
|
||||
@ -122,8 +133,8 @@ class WebLog {
|
||||
buffer = "";
|
||||
}
|
||||
|
||||
// Clear the debug log over the Internet to start a new track of the watch's runtime
|
||||
// execution.
|
||||
//! Clear the debug log over the Internet to start a new track of the watch's runtime
|
||||
//! execution.
|
||||
//
|
||||
function clear() {
|
||||
// System.println("WebLog clear()");
|
||||
@ -143,7 +154,10 @@ class WebLog {
|
||||
buffer = "";
|
||||
}
|
||||
|
||||
// Callback function to print the outcome of a doPrint() method.
|
||||
//! Callback function to print the outcome of a doPrint() method. Typically used for debugging this class.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
function onLog(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
// if (responseCode != 200) {
|
||||
@ -153,7 +167,10 @@ class WebLog {
|
||||
// }
|
||||
}
|
||||
|
||||
// Callback function to print the outcome of a clear() method.
|
||||
// Callback function to print the outcome of a clear() method. Typically used for debugging this class.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
function onClear(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
// if (responseCode != 200) {
|
||||
|
@ -11,14 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey, 10 January 2024
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant Webhook creation.
|
||||
//
|
||||
// Reference:
|
||||
// * https://developers.home-assistant.io/docs/api/native-app-integration
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -26,10 +18,24 @@ using Toybox.Communications;
|
||||
using Toybox.System;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
// Can use push view so must never be run in a glance context
|
||||
//! Home Assistant Webhook creation.
|
||||
//!
|
||||
//! NB. Because we can use push view (E.g. `ErrorView.show()`) this class must never
|
||||
//! be run in a glance context.
|
||||
//!
|
||||
//! Reference: https://developers.home-assistant.io/docs/api/native-app-integration
|
||||
//
|
||||
class WebhookManager {
|
||||
|
||||
function onReturnRequestWebhookId(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
//! Callback for requesting a Webhook ID.
|
||||
//!
|
||||
//! @param responseCode Response code
|
||||
//! @param data Return data
|
||||
//
|
||||
function onReturnRequestWebhookId(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String
|
||||
) as Void {
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
@ -84,6 +90,8 @@ class WebhookManager {
|
||||
}
|
||||
}
|
||||
|
||||
//! Request a Webhook ID from Home Assistant for use in this application.
|
||||
//
|
||||
function requestWebhookId() {
|
||||
var deviceSettings = System.getDeviceSettings();
|
||||
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
|
||||
@ -115,30 +123,37 @@ class WebhookManager {
|
||||
);
|
||||
}
|
||||
|
||||
function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, sensors as Lang.Array<Lang.Object>) as Void {
|
||||
//! Callback function for the POST request to register the watch's sensors on the Home Assistant instance.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//! @param sensors The remaining sensors to be processed. The list of sensors is iterated through
|
||||
//! until empty. Each POST request creating one sensor on the local Home Assistant.
|
||||
//
|
||||
function onReturnRegisterWebhookSensor(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String,
|
||||
sensors as Lang.Array<Lang.Object>
|
||||
) as Void {
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
|
||||
Settings.unsetWebhookId();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.BLE_QUEUE_FULL:
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
|
||||
Settings.unsetWebhookId();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
|
||||
Settings.unsetWebhookId();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
|
||||
Settings.unsetWebhookId();
|
||||
// Ignore and see if we can carry on
|
||||
break;
|
||||
|
||||
@ -198,7 +213,11 @@ class WebhookManager {
|
||||
}
|
||||
}
|
||||
|
||||
function registerWebhookSensor(sensors as Lang.Array<Lang.Object>) {
|
||||
//! Local method to send the POST request to register a number of sensors.
|
||||
//!
|
||||
//! @param sensors An array of sensors, e.g. As created by `registerWebhookSensors()`.
|
||||
//
|
||||
private function registerWebhookSensor(sensors as Lang.Array<Lang.Object>) {
|
||||
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
|
||||
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
|
||||
// System.println("WebhookManager registerWebhookSensor(): URL=" + url);
|
||||
@ -221,6 +240,8 @@ class WebhookManager {
|
||||
);
|
||||
}
|
||||
|
||||
//! Request the creation of all the supported watch sensors on the Home Assistant instance.
|
||||
//
|
||||
function registerWebhookSensors() {
|
||||
var heartRate = Activity.getActivityInfo().currentHeartRate;
|
||||
|
||||
|
@ -7,13 +7,13 @@
|
||||
<link
|
||||
rel="stylesheet"
|
||||
data-name="vs/editor/editor.main"
|
||||
href="https://unpkg.com/monaco-editor@0.45.0/min/vs/editor/editor.main.css" />
|
||||
href="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/editor/editor.main.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://unpkg.com/toastify-js@1.12.0/src/toastify.css" />
|
||||
href="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.css" />
|
||||
<style>
|
||||
@import url('https://unpkg.com/@catppuccin/palette/css/catppuccin.css');
|
||||
@import url('https://www.unpkg.com/@catppuccin/palette/css/catppuccin.css');
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
@ -37,7 +37,7 @@
|
||||
margin-left: 0.5em;
|
||||
filter: grayscale() invert();
|
||||
}
|
||||
.template {
|
||||
.template, .info {
|
||||
background-image: url(../resources-icons-48/info_type.svg);
|
||||
background-size: contain;
|
||||
margin-left: 0.5em;
|
||||
@ -441,9 +441,9 @@ http:
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script src="https://unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
|
||||
<script src="https://unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
|
||||
<script src="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
|
||||
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -451,7 +451,7 @@ loadSchema();
|
||||
// require is provided by loader.min.js.
|
||||
require.config({
|
||||
paths: {
|
||||
vs: 'https://unpkg.com/monaco-editor@0.45.0/min/vs',
|
||||
vs: 'https://www.unpkg.com/monaco-editor@0.45.0/min/vs',
|
||||
},
|
||||
});
|
||||
require(['vs/editor/editor.main'], async () => {
|
||||
|