Compare commits

..

54 Commits

Author SHA1 Message Date
Joseph Abbey
75045b19d9 Fix for numeric menu items over Wi-Fi/LTE (#318)
Turns out the sync service will not run unless the picker has not only
been popped but the display also updated.

v3.7 needs a swift turnaround as its a bug reported by a user.
2025-11-07 23:23:42 +00:00
Philip Abbey
d6e32b777f Update HISTORY.md
Added v3.7 text.
2025-11-07 09:10:47 +00:00
Philip Abbey
1e17d93310 Fix for numeric menu items over Wi-Fi/LTE
Turns out the sync service will not run unless the picker has not only been popped but the display also updated.
2025-11-07 09:05:22 +00:00
Philip Abbey
cc53b25508 Update HomeAssistantTapMenuItem.mc
Code tidy only.
2025-11-07 09:03:05 +00:00
Philip Abbey
098dc81236 Space alignment in code (tidy) and v3.6 history amendment 2025-11-06 20:40:43 +00:00
Philip Abbey
5ab8229602 Update Numeric.md
Changed 'service' to 'action'. Careless omission for new documentation.

Signed-off-by: Philip Abbey <philipabbey@users.noreply.github.com>
2025-11-06 18:15:09 +00:00
Philip Abbey
8acc450c2c Update HISTORY.md
Amended v3.6 description.
2025-11-04 21:22:48 +00:00
Philip Abbey
1a52a942fd Added two screenshots for the Number Picker to Numeric.md (#316)
Amendment to documentation. Overlaps with changes a couple of changes on
#308 due to order.
2025-11-04 20:50:51 +00:00
Philip Abbey
d8b82f23e4 Merge branch 'main' into 315-add-screenshots-to-numericmd
Signed-off-by: Philip Abbey <philipabbey@users.noreply.github.com>
2025-11-04 20:50:24 +00:00
Philip Abbey
4943c87edb Icon script changes (#314)
A partial update that does not include managing the white variants of
the icons.
2025-11-04 20:49:22 +00:00
Philip Abbey
179c4d1bc5 Rename service to action (#308) 2025-11-04 20:48:58 +00:00
Joseph Abbey
6822cbe434 Allow floats for media_player picker limits 2025-11-04 19:09:45 +00:00
Joseph Abbey
fbeadf7ba9 Merge branch 'main' into rename-service-to-action 2025-11-04 19:02:19 +00:00
Joseph Abbey
b688cec8f6 Use minimum and maximum instead of const 2025-11-04 19:00:22 +00:00
Philip Abbey
4119665817 Added two screenshots for the Number Picker to Numeric.md 2025-11-02 23:15:59 +00:00
Philip Abbey
f0e263ae54 Update config.schema.json
Amended "Home Assistant Template" description.
2025-11-02 22:38:19 +00:00
Philip Abbey
abd6552916 Documentation changes
"Home Assistant" => "HomeAssistant" search & replace
Added schema change for "exit" feature.
Added initial Devices.md
2025-11-02 19:36:33 +00:00
Philip Abbey
60f754f3e3 Update HomeAssistantMenuItemFactory.mc
Reverting picker => data change, as I think the original was correct, compiled and functionally worked.
2025-11-02 17:41:21 +00:00
Joseph Abbey
bc5a7d04e4 Schema fixes and move "exit" into "tap_action" 2025-11-02 13:14:48 +00:00
Philip Abbey
643c4aa2e5 Icon script changes
A partial update that does not include managing the white variants of the icons,
2025-11-02 13:14:31 +00:00
Philip Abbey
8360a3e4a2 Documentation update
Mainly for the HTTP 410 error case.
2025-11-02 12:20:46 +00:00
Joseph Abbey
ad83988ade Merge branch 'main' into rename-service-to-action
Signed-off-by: Joseph Abbey <me@josephabbey.dev>
2025-11-01 21:23:08 +00:00
Joseph Abbey
cac94fecd4 rename service to action 2025-11-01 21:19:10 +00:00
Philip Abbey
f9253e8cf0 Json schema update (#310) 2025-11-01 19:05:11 +00:00
Philip Abbey
3528080ec3 Merge branch 'main' into json-schema-update 2025-11-01 19:03:34 +00:00
Philip Abbey
cc321899f4 Initial code for user supplied confirmation messages (#307)
Hopefully a simple change to the code. This is on top of #306.
2025-11-01 19:02:56 +00:00
Philip Abbey
5a44765ac9 305 code tidy documentation for v36 (#306)
1. Amended `Lang.Dictionary` handling so more of them use the
`dict["key"] = value` format.
2. Added documentation for the `numeric` menu type.
2025-11-01 19:02:25 +00:00
Philip Abbey
9eb791c68b Update Numeric.md
Review comments.
2025-11-01 18:57:13 +00:00
Joseph Abbey
2fca0ef3a3 Update schema for picker object 2025-11-01 18:26:24 +00:00
Philip Abbey
fc0320aef6 Initial code for user supplied confirmation messages 2025-10-30 17:50:04 +00:00
Philip Abbey
0d3c76ef2e Update config.schema.json
Made the 'picker' field within 'tap_action' mandatory for a 'numeric' menu items.
2025-10-30 17:03:21 +00:00
Philip Abbey
4c946d584a Update HISTORY.md 2025-10-30 16:36:06 +00:00
Philip Abbey
6e3cf73ab3 Update manifest.xml
Reverted the application ID to one of the project's.
2025-10-30 11:50:56 +00:00
Philip Abbey
14186b7992 Documentation & source tidy for Lang.Dictionary items. 2025-10-30 11:39:19 +00:00
Philip Abbey
f64bed5058 Add light effect selector example to Select.md (#301)
Added an example of a light effect selector in JSON format.
2025-10-30 09:03:58 +00:00
Philip Abbey
619671de5d Moved the contents of Select.md to Actions.md
Feels like we already have a home for the example without creating a separate new file.
2025-10-30 09:02:40 +00:00
Philip Abbey
6d18406880 Select schema version in web (#300)
To test the schema on a specific version:
```url
https://house-of-abbey.github.io/web/?version=v1.4
```
To test the schema on a specific branch:
```url
https://house-of-abbey.github.io/web/?branch=numeric-item-json-schema
```
To test the schema on an arbitrary URL (may be affected by cors):
```url
https://house-of-abbey.github.io/web/?schema={url}
```
2025-10-30 08:57:04 +00:00
Philip Abbey
3a7676f4bf Add Numeric Menu Item (#298)
Added a new numeric menu item to set numeric values e.g. for heating,
volume, dimmer etc.
2025-10-29 20:11:31 +00:00
thmichel
f19eb7c276 Fixed compiler warning for unreachable code 2025-10-29 19:18:06 +01:00
thmichel
c617d2cad6 Merge pull request #4 from house-of-abbey/Picker-formatter
Suggested code changes from philipabbey
2025-10-29 18:33:54 +01:00
Philip Abbey
d1f6f6d9d2 Deduped picker variable 2025-10-29 15:14:30 +00:00
Philip Abbey
35333f4d75 Merge branch 'pr/298' into Picker-formatter 2025-10-29 15:13:13 +00:00
Philip Abbey
a5ddb65512 Suggested code changes from philipabbey
1. attribute is option, so needs a different template in the API call when absent.
2. Automatically derive the format string from the picker step value for any precision of step.
3. Changed all Lang.String representations of numbers to Lang.Number or Lang.Float. I'm keen to remove the use of strings to hold a numeric value.
4. Tidied up and completed some code comments.
5. Adjusted the JSON schema definition. This is still not finished as the 'picker' object is required for 'numeric' menu items and must not be present for the others. Additional schema changes are required for greater precision.
6. Moved fields over from 'data' to 'picker'.
2025-10-29 14:26:02 +00:00
thmichel
b0fa10b2c1 Fixed typo in formatsgtring and error if numeric template didn't return a value 2025-10-29 14:41:32 +01:00
thmichel
6a0ec34cdb Using a picker object to configure the picker now, deriving display format from steps. 2025-10-29 13:54:14 +01:00
thmichel
2cd171637c Reworked numericMenuItem to be able to display a different conten in the sublabel than jus a number. 2025-10-25 21:26:44 +02:00
thmichel
264b160fdf Merge pull request #3 from house-of-abbey/numeric-item-json-schema
Update schema to support numeric items
2025-10-23 12:10:20 +02:00
Joseph Abbey
81fa876449 Add light effect selector example to Select.md
Added an example of a light effect selector in JSON format.

Signed-off-by: Joseph Abbey <me@josephabbey.dev>
2025-10-22 14:37:41 +01:00
Joseph Abbey
b563ab7923 Arbitrary schema URL 2025-10-22 09:08:24 +01:00
Joseph Abbey
2ebf36a445 Select schema version in web 2025-10-22 09:05:09 +01:00
Joseph Abbey
5bdab41d8b Fix examples lists 2025-10-21 19:10:35 +01:00
Joseph Abbey
85080f5d46 2025-10-21 10:07:43 +01:00
Joseph Abbey
35e0fe26d0 Amendments 2025-10-21 09:05:15 +01:00
Joseph Abbey
427c1834a8 2025-10-21 09:03:49 +01:00
39 changed files with 1509 additions and 656 deletions

View File

@@ -1,8 +1,8 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# Background Service
The background service can report the following statuses from your device to your Home Assistant:
The background service can report the following statuses from your device to your HomeAssistant:
- Battery Level with charging status.
- Location and location accuracy.
@@ -12,13 +12,13 @@ If your device does not support the background service, the application will cle
## Limits
The values are merely samples of your device's current status. They are sent by a single background service at the repetition frequency you chose in the settings. The samples are sent at that one rate only, they _do not vary_ for example on in activity, on charge, time of day. You get one refresh interval and that is it. If you want to change the refresh interval, you change your settings. We do appreciate that may not be what you would ideally like to trigger actions on Home Assistant. Messing with the repeat interval of the background service requires more code, more settings and more complexity. That means older devices using widgets would have to be taken out of support to achieve it.
The values are merely samples of your device's current status. They are sent by a single background service at the repetition frequency you chose in the settings. The samples are sent at that one rate only, they _do not vary_ for example on in activity, on charge, time of day. You get one refresh interval and that is it. If you want to change the refresh interval, you change your settings. We do appreciate that may not be what you would ideally like to trigger actions on HomeAssistant. Messing with the repeat interval of the background service requires more code, more settings and more complexity. That means older devices using widgets would have to be taken out of support to achieve it.
**Please do not ask for these to be made 'events'.** Garmin's [Connect IQ background service](https://developer.garmin.com/connect-iq/api-docs/Toybox/System/ServiceDelegate.html) is limited in that while it does provide an `onActivityCompleted()` method, it does not provide an `onActivityStarted()` method, so you would not have the complete activity life cycle anyway. So we're keeping this implementation simple, you just get a sampling at one refresh rate. This probably limits you to updating a status on a Home Assistant Dashboard only.
**Please do not ask for these to be made 'events'.** Garmin's [Connect IQ background service](https://developer.garmin.com/connect-iq/api-docs/Toybox/System/ServiceDelegate.html) is limited in that while it does provide an `onActivityCompleted()` method, it does not provide an `onActivityStarted()` method, so you would not have the complete activity life cycle anyway. So we're keeping this implementation simple, you just get a sampling at one refresh rate. This probably limits you to updating a status on a HomeAssistant Dashboard only.
## Battery Reporting
From version 2.1 the application includes a background service to report the current device battery level and charging status back to Home Assistant. This is a feature that Garmin omitted to include with the Bluetooth connection.
From version 2.1 the application includes a background service to report the current device battery level and charging status back to HomeAssistant. This is a feature that Garmin omitted to include with the Bluetooth connection.
## Location Reporting
@@ -47,7 +47,7 @@ From version 2.6 the application includes reporting your activity. The activity
- Activity - This is an integer as defined by [Toybox.Activity `SPORT`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module)
- Sub-activity - This is an integer as defined by [Toybox.Activity `SUB_SPORT`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#SubSport-module)
The application only provides the integers without translation. When using the values in Home Assistant, you will need to provide you own mapping from the `Activity` enumerated type to the human readable text. As developers of the application we are pushing this translation to the server to keep the Garmin application code 'lean'. You will also need to add to both the list of activities (sports) and sub-activities (sub-sports) an interpretation of integer `-1` for no activity/sub-activity at present.
The application only provides the integers without translation. When using the values in HomeAssistant, you will need to provide you own mapping from the `Activity` enumerated type to the human readable text. As developers of the application we are pushing this translation to the server to keep the Garmin application code 'lean'. You will also need to add to both the list of activities (sports) and sub-activities (sub-sports) an interpretation of integer `-1` for no activity/sub-activity at present.
## Start Reporting
@@ -55,7 +55,7 @@ The main drawback of this solution is that the Garmin application must be run on
It should be as simple as starting the application (or widget). There should be a new device in the mobile app integration called `Garmin Watch` with the battery level and charging status.
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=mobile_app)
[![Open your HomeAssistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=mobile_app)
If this is not the case, head over to the [troubleshooting page](Troubleshooting.md#watch-battery-level-reporting).
@@ -67,7 +67,7 @@ To stop the reporting, the option must be turned off in the settings and then th
When the device is first created, it will be called `Garmin Watch`. This can be changed in the mobile app integration settings (button below).
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=mobile_app)
[![Open your HomeAssistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=mobile_app)
Select the device called `Garmin Watch` and then click on the edit icon in the top right corner. You can then change the name of the device to whatever you like, then press `UPDATE` and then `RENAME`.
@@ -91,7 +91,7 @@ template:
icon: "mdi:battery{% if is_state('binary_sensor.<device>_battery_is_charging', 'on') %}-charging{% endif %}{% if 0 < (states('sensor.<device>_battery_level') | float / 10 ) | round(0) * 10 < 100 %}-{{ (states('sensor.<device>_battery_level') | float / 10 ) | round(0) * 10 }}{% else %}{% if (states('sensor.<device>_battery_level') | float / 10 ) | round(0) * 10 == 0 %}-outline{% else %}{% if is_state('binary_sensor.<device>_battery_is_charging', 'on') %}-100{% endif %}{% endif %}{% endif %}"
```
## Adding a sample Home Assistant UI widget
## Adding a sample HomeAssistant UI widget
A gauge for battery level with a charging icon making use of [mushroom cards](https://github.com/piitaya/lovelace-mushroom), [card_mod](https://github.com/thomasloven/lovelace-card-mod) and [stack-in-card](https://github.com/custom-cards/stack-in-card):
@@ -161,7 +161,7 @@ N.B. `sensor.<device>_battery_level` will likely need to be changed to `sensor.<
## Migrating
You should remove your old template sensors before migrating to the new integration. You can do this by removing the `sensor.<device>_battery_level` and `binary_sensor.<device>_battery_is_charging` entities from `configuration.yaml` and then restarting Home Assistant or reloading the YAML.
You should remove your old template sensors before migrating to the new integration. You can do this by removing the `sensor.<device>_battery_level` and `binary_sensor.<device>_battery_is_charging` entities from `configuration.yaml` and then restarting HomeAssistant or reloading the YAML.
[Here is the old configuration method for reference.](https://github.com/house-of-abbey/GarminHomeAssistant/blob/b51e2aa2a4afbc58ad466f3b81667d1cd252d091/BatteryReporting.md)

194
Devices.md Normal file
View File

@@ -0,0 +1,194 @@
# Device Support & Characterisation
A page just to note a practical limit on support for some older devices.
## Application Memory Usage
On an `instinct2x` device:
| Version | Free Memory (bytes) on `instinct2x`| Free Memory (bytes) on `venu2`|
|:-------:|-----------------------------------:|------------------------------:|
| 3.5 | 62,360 | - |
| 3.6 | 65,696 | 53,832 |
A user has reported a maximum of 26 items with Ver 3.5. This measurement has shown that each menu item requires about 1.0~1.2 kB. Using the worked example below it is possible to predict how many menu items your particular device might be able to support by using indicative figures.
## Worked Example
As a worked example, for Ver 3.6 working on an `instinct2x` device:
| Feature | Memory (bytes) | Cost (bytes) |
|--------------------------------------|---------------:|-------------:|
| Declared available to application | 98,304 | |
| Measured available to application | 94,112 | (4,192 less) |
| Application used | 65,696 | |
| Free before fetching menu definition | 28,416 | |
| Free after fetching menu definition | 15,792 | 12,624 |
| Free after construction | 936 | 14,856 |
Our test menu presently contains a mix of 28 items, consisting of nested group, toggle, tap, info and numeric items with templates. So each item requires (12,624 + 14,856) / 28 = 982 bytes.
## Garmin Devices
The following table details all the devices as at 1 October 2025 and whether they are supported by Garmin HomeAssistant. The available application memory is also detailed so that it can be compared to an application version listed above. Of particular concern are the 'Instinct' range of devices, being the smallest we currently support. New feature requests are now being vetted against how they might affect our ability to support the 'Instinct' range of devices. At some point support may have to be withdrawn in order to allow the Garmin HomeAssistant application to grow further.
| Device | Supported | Application Memory |
|----------------------------|:---------:|--------------------:|
| d2bravo | N | 65,536 |
| d2bravo_titanium | N | 65,536 |
| fenix3 | N | 65,536 |
| fenix3_hr | N | 65,536 |
| fr230 | N | 65,536 |
| fr235 | N | 65,536 |
| fr630 | N | 65,536 |
| fr920xt | N | 65,536 |
| vivoactive | N | 65,536 |
| descentg1 | Y | 98,304 |
| instinct2 | Y | 98,304 |
| instinct2s | Y | 98,304 |
| instinct2x | Y | 98,304 |
| instinctcrossover | Y | 98,304 |
| approachs60 | N | 131,072 |
| enduro | Y | 131,072 |
| fenix5 | Y | 131,072 |
| fenix5s | Y | 131,072 |
| fenix6 | Y | 131,072 |
| fenix6s | Y | 131,072 |
| fenixchronos | Y | 131,072 |
| fr245 | Y | 131,072 |
| fr55 | Y | 131,072 |
| fr645 | Y | 131,072 |
| fr735xt | N | 131,072 |
| fr935 | Y | 131,072 |
| instinct3solar45mm | Y | 131,072 |
| instincte40mm | Y | 131,072 |
| instincte45mm | Y | 131,072 |
| venusq | Y | 131,072 |
| vivoactive3 | Y | 131,072 |
| vivoactive3d | N | 131,072 |
| vivoactive_hr | N | 131,072 |
| edge_520 | N | 262,144 |
| fr255 | Y | 524,288 |
| fr255s | Y | 524,288 |
| approachs50 | Y | 786,432 |
| approachs7042mm | Y | 786,432 |
| approachs7047mm | Y | 786,432 |
| d2airx10 | Y | 786,432 |
| d2mach1 | Y | 786,432 |
| descentg2 | Y | 786,432 |
| descentmk343mm | Y | 786,432 |
| descentmk351mm | Y | 786,432 |
| enduro3 | Y | 786,432 |
| epix2 | Y | 786,432 |
| epix2pro42mm | Y | 786,432 |
| epix2pro47mm | Y | 786,432 |
| epix2pro47mmsystem7preview | Y | 786,432 |
| epix2pro51mm | Y | 786,432 |
| fenix7 | Y | 786,432 |
| fenix7pro | Y | 786,432 |
| fenix7pronowifi | Y | 786,432 |
| fenix7s | Y | 786,432 |
| fenix7spro | Y | 786,432 |
| fenix7x | Y | 786,432 |
| fenix7xpro | Y | 786,432 |
| fenix7xpronowifi | Y | 786,432 |
| fenix843mm | Y | 786,432 |
| fenix847mm | Y | 786,432 |
| fenix8pro47mm | Y | 786,432 |
| fenix8solar47mm | Y | 786,432 |
| fenix8solar51mm | Y | 786,432 |
| fenixe | Y | 786,432 |
| fr165 | Y | 786,432 |
| fr165m | Y | 786,432 |
| fr255m | Y | 786,432 |
| fr255sm | Y | 786,432 |
| fr265 | Y | 786,432 |
| fr265s | Y | 786,432 |
| fr57042mm | Y | 786,432 |
| fr57047mm | Y | 786,432 |
| fr955 | Y | 786,432 |
| fr965 | Y | 786,432 |
| fr970 | Y | 786,432 |
| instinct3amoled45mm | Y | 786,432 |
| instinct3amoled50mm | Y | 786,432 |
| instinctcrossoveramoled | Y | 786,432 |
| marq2 | Y | 786,432 |
| marq2aviator | Y | 786,432 |
| system8preview | N | 786,432 |
| venu2 | Y | 786,432 |
| venu2plus | Y | 786,432 |
| venu2s | Y | 786,432 |
| venu3 | Y | 786,432 |
| venu3s | Y | 786,432 |
| venu441mm | Y | 786,432 |
| venu445mm | Y | 786,432 |
| venusq2 | Y | 786,432 |
| venusq2m | Y | 786,432 |
| venux1 | Y | 786,432 |
| vivoactive5 | Y | 786,432 |
| vivoactive6 | Y | 786,432 |
| approachs62 | N | 1,048,576 |
| d2air | Y | 1,048,576 |
| edge1030 | Y | 1,048,576 |
| edge1030bontrager | Y | 1,048,576 |
| edge1030plus | Y | 1,048,576 |
| edge1040 | Y | 1,048,576 |
| edge1050 | Y | 1,048,576 |
| edge520plus | Y | 1,048,576 |
| edge530 | Y | 1,048,576 |
| edge540 | Y | 1,048,576 |
| edge550 | Y | 1,048,576 |
| edge820 | Y | 1,048,576 |
| edge830 | Y | 1,048,576 |
| edge840 | Y | 1,048,576 |
| edge850 | Y | 1,048,576 |
| edgeexplore | Y | 1,048,576 |
| edgeexplore2 | Y | 1,048,576 |
| edgemtb | Y | 1,048,576 |
| edge_1000 | N | 1,048,576 |
| epix | N | 1,048,576 |
| fr645m | Y | 1,048,576 |
| legacyherocaptainmarvel | Y | 1,048,576 |
| legacyherofirstavenger | Y | 1,048,576 |
| legacysagadarthvader | Y | 1,048,576 |
| legacysagarey | Y | 1,048,576 |
| venu | Y | 1,048,576 |
| venud | Y | 1,048,576 |
| venusqm | Y | 1,048,576 |
| vivoactive3m | Y | 1,048,576 |
| vivoactive3mlte | Y | 1,048,576 |
| vivoactive4 | Y | 1,048,576 |
| vivoactive4s | Y | 1,048,576 |
| d2charlie | N | 1,310,720 |
| d2delta | Y | 1,310,720 |
| d2deltapx | Y | 1,310,720 |
| d2deltas | Y | 1,310,720 |
| descentmk1 | N | 1,310,720 |
| descentmk2 | Y | 1,310,720 |
| descentmk2s | Y | 1,310,720 |
| fenix5plus | Y | 1,310,720 |
| fenix5splus | Y | 1,310,720 |
| fenix5x | Y | 1,310,720 |
| fenix5xplus | Y | 1,310,720 |
| fenix6pro | Y | 1,310,720 |
| fenix6spro | Y | 1,310,720 |
| fenix6xpro | Y | 1,310,720 |
| fr245m | Y | 1,310,720 |
| fr745 | Y | 1,310,720 |
| fr945 | Y | 1,310,720 |
| fr945lte | Y | 1,310,720 |
| marqadventurer | Y | 1,310,720 |
| marqathlete | Y | 1,310,720 |
| marqaviator | Y | 1,310,720 |
| marqcaptain | Y | 1,310,720 |
| marqcommander | Y | 1,310,720 |
| marqdriver | Y | 1,310,720 |
| marqexpedition | Y | 1,310,720 |
| marqgolfer | Y | 1,310,720 |
| gpsmap66 | Y | 2,359,296 |
| gpsmap67 | Y | 2,359,296 |
| gpsmap86 | N | 2,359,296 |
| gpsmaph1 | Y | 2,359,296 |
| montana7xx | Y | 2,359,296 |
| oregon7xx | N | 2,359,296 |
| rino7xx | N | 2,359,296 |

View File

@@ -1,4 +1,4 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# Version History
@@ -49,5 +49,7 @@
| 3.1 | Added the ability for users to provide [custom HTTP headers](HTTP_Headers.md) for their HomeAssistant server. Improved German language translations. Thanks to [@tispokes](https://github.com/tispokes) for assisting with both of those. Removed all groups in settings as the SDK is buggy. Fixed a bug with templates in glances causing application crash on startup. |
| 3.2 | Only enable or disable sensors on HomeAssistant when the background service options is changed, i.e. do not call the API to enable on start up every time. |
| 3.3 | Providing automatic detection for menu definition updates, but still requires an application restart. |
| 3.4 | Fixed a bug where templates failed to display in toggle menu items (at least on some devices). Fixed a bug where a menu item requesting to exit on completion appeared to indicate failure when using Wi-Fi or LTE. The fix uses a delay in exiting the application modelled as sufficient for a Venu 2 device, so this might need tweaking for other devices. Attempt to fixed an "Out of Memory" bug caused by v3.3 by making automatic checking for menu updates both optional and automatically turned off when insufficient memory is available. This last bug is device dependent and may require another attempt. Internationalisation improvements with thanks to @krzys_h for a new automated translations script. |
| 3.4 | Fixed a bug where templates failed to display in toggle menu items (at least on some devices). Fixed a bug where a menu item requesting to exit on completion appeared to indicate failure when using Wi-Fi or LTE. The fix uses a delay in exiting the application modelled as sufficient for a Venu 2 device, so this might need tweaking for other devices. Attempt to fixed an "Out of Memory" bug caused by v3.3 by making automatic checking for menu updates both optional and automatically turned off when insufficient memory is available. This last bug is device dependent and may require another attempt. Internationalisation improvements with thanks to [@krzys_h](https://github.com/krzys-h) for a new automated translations script. |
| 3.5 | Added support for Edge 550, 850 & MTB, Fenix 8 Pro 47mm, GPSMAP H1, Instinct Crossover AMOLED, Venu 4 41mm & 45mm, & Venu X1 devices which also required an SDK update to 8.3.0. The simulation of the Edge 850 device was off, as it failed to update the display and text was the wrong colour, but the buttons menu items operated HA correctly. The assumption is the simulation model is buggy until someone [reports](https://github.com/house-of-abbey/GarminHomeAssistant/issues) otherwise. |
| 3.6 | Added `numeric` menu item type thanks to [@thmichel](https://github.com/thmichel). This allows you to select a numeric value to set for an entity. Confirmations can now display a user supplied message. [Schema update](README.md#old-deprecated-formats) to keep pace with HomeAssistant and correct a previous decision. Schema changes for consistency. |
| 3.7 | Bug fix for `numeric` menu items not working over Wi-Fi & LTE. |

View File

@@ -1,4 +1,4 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# User Specified Custom HTTP Headers

135
README.md
View File

@@ -1,4 +1,4 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# GarminHomeAssistant
@@ -145,22 +145,21 @@ Example schema:
}
},
{
"name": "Heating",
"content": "{{ ' %.1f' | format(state_attr('climate.myheating','temperature')) }}",
"type": "numeric",
"entity": "climate.myheating",
"tap_action": {
"action": "climate.set_temperature",
"data": {
"step": "0.5",
"start": "10",
"stop": "30",
"valueLabel": "temperature",
"formatString": "%.1f"
}
},
"pin": false
} ,
"name": "Heating",
"content": "{{ ' %.1f' | format(state_attr('climate.room','temperature')) }}",
"type": "numeric",
"entity": "climate.room",
"tap_action": {
"action": "climate.set_temperature",
"picker": {
"step": 0.5,
"start": 10,
"stop": 30,
"attribute": "temperature",
"data_attribute": "temperature"
}
}
}
]
}
```
@@ -172,35 +171,47 @@ 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`)
* Action invocation, e.g. Scene setting, (`tap`)
* A sub-menu to open (`group`)
* A numeric item (`numeric`), which allows you to set a numeric value e.g. for heating or a dimmer. ValueLabel defines the variable to return. You can optionally set the minimum (start) and maximum (stop) value as well as the step to increase/decrease and a tepmlate how to format the value.
* A numeric item (`numeric`), which allows you to set a numeric value e.g. for heating or a dimmer. This is [explained more fully](examples/Numeric.md) in its own examples page.
* 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 HomeAssistant 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 | Info (status)|
|------------------|:---:|:------:|:------------:|
| Switch | ❌ | ✅ | ✅ |
| Light | ❌ | ✅ | ✅ |
| Automation | ✅ | | |
| Script | ✅ | | |
| Scene | ✅ | ❌ | ❌ |
| Sensor | | ❌ | |
| Binary Sensor | ❌ | ❌ | ✅ |
| Any other entity | ❌ | ❌ | ✅ |
| Any service | | ❌ | |
| HA Entity Type | Tap | Toggle | Info (status)| Numeric |
|------------------|:---:|:------:|:------------:|:-------:|
| Switch | ❌ | ✅ | ✅ | ❌ |
| Switched Light | ❌ | ✅ | ✅ | ❌ |
| Dimmer Light | ❌ | ❌ | | |
| Automation | ✅ | ✅ | | |
| Script | ✅ | ❌ | ❌ | ❌ |
| Scene | | ❌ | | ❌ |
| Sensor | ❌ | ❌ | ✅ | ❌ |
| Binary Sensor | ❌ | ❌ | ✅ | ❌ |
| Thermostat | | ❌ | ✅ | |
| Amplifier | ❌ | ❌ | ✅ | ✅ |
| Any other entity | ❌ | ❌ | ✅ | ❌ |
| Any action | ✅ | ❌ | ❌ | ❌ |
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).
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` and `numeric` items must specify a `action` 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 HomeAssistant. 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
### Old Deprecated Formats
Version 1.5 brought in a change to the JSON schema so the following old format remains useable but is no longer favoured. The schema now marks it as 'deprecated' to nudge people over.
There are two reasons for the changes to the schema:
1. HomeAssistant made changes we feel we should track for consistency.
2. Retrospectively we decided there was a better way, just like HomeAssistant did. For these changes we apologise.
#### Service Field
Version 1.5 brought in a change to the JSON schema so the following old format remains useable but is no longer favoured.
> [!IMPORTANT] Deprecated:
```json
{
@@ -211,7 +222,9 @@ Version 1.5 brought in a change to the JSON schema so the following old format r
}
```
The above should be replaced by the following:
Version 3.6 brought another change to the JSON schema to follow HomeAssistant's renaming of `service` to `action`.
> [!IMPORTANT] Deprecated:
```json
{
@@ -224,18 +237,66 @@ The above should be replaced by the following:
}
```
This allows the `confirm` and `pin` fields to be accommodated in the `tap_action` along side the `service` tag, and follows the HomeAssistant YAML format more closely.
The above should be replaced by the following:
```json
{
"entity": "scene.tv_light",
"name": "TV Lights Scene",
"type": "tap",
"tap_action": {
"action": "scene.turn_on"
}
}
```
This allows the `confirm` and `pin` fields to be accommodated in the `tap_action` along side the `action` tag, and follows the HomeAssistant YAML format more closely.
#### Exit Field
Version 2.31 added an "exit on tap" feature. In retrospect this field should have been nested inside the `tap_action` object.
> [!IMPORTANT] Deprecated:
```json
{
"entity": "automation.turn_off_stuff",
"name": "Turn off Stuff",
"type": "tap",
"tap_action": {
"action": "automation.trigger"
},
"exit": true
}
```
The above should be replaced by the following:
```json
{
"entity": "automation.turn_off_stuff",
"name": "Turn off Stuff",
"type": "tap",
"tap_action": {
"action": "automation.trigger",
"exit": true
},
}
```
A future move to v3.x will remove support for all deprecated JSON elements to simplify code. **Please ensure you track the schema changes in readiness.**
### More Examples
* [Switches](examples/Switches.md)
* [Actions](examples/Actions.md)
* [Templates](examples/Templates.md)
* [Numeric](examples/Numeric.md)
## Editing the JSON file
You have options. The first is what we use.
1. **Best!** Use the GarminHomeAssistant [Web-based Editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) which includes `entity` and `service` name completion and validation by fetching data from your own HomeAssistant instance. _Pretty nifty eh?_ The other method listed below do not add this convenience and checking.
1. **Best!** Use the GarminHomeAssistant [Web-based Editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) which includes `entity` and `action` name completion and validation by fetching data from your own HomeAssistant instance. _Pretty nifty eh?_ The other method listed below do not add this convenience and checking.
2. Use the [Studio Code Server](https://community.home-assistant.io/t/home-assistant-community-add-on-visual-studio-code/107863) addon for HomeAssistant. You can then edit your JSON file in place.
3. Locally installed VSCode, or if not installed, try
4. The on-line version at https://vscode.dev/, which works really well.
@@ -382,6 +443,8 @@ Check the latest unresolved [issues](https://github.com/house-of-abbey/GarminHom
9. When using Wi-Fi or LTE to toggle a light, the `toggle` will fail when the default or current state of the application's menu does not match the state of the light. The same applies to a cover or other thing that can be toggled. This is because the application is unable to initialise the menu with the current state without Bluetooth. Hence the Wi-Fi/LTE functionality is best used with `tap` items only.
10. There are memory limits, particularly for older devices. Please see the [explanation of the memory limits](Devices.md) and device support.
# Authors & Contributors
For an up to date list of all authors and contributors, please check the [contributor's page](https://github.com/house-of-abbey/GarminHomeAssistant/graphs/contributors). Thank you all for improving this application.

View File

@@ -1,4 +1,4 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# Troubleshooting Guides
@@ -315,9 +315,15 @@ JSON for copy & paste:
![No JSON](images/NoJson.png)
When the application persists in reporting "No JSON returned from HTTP request." this might be due to a mismatch between the Webhook ID and the device settings on the HomeAssistant server. We have discovered that the Webhook ID is required for HomeAssistant API calls with templates in order to work in a non-privileged account. The application options include the ability to clear the Webhook ID in the application forcing a new one to be set up. This should prevent the above error being shown on startup.
When the application persists in reporting _"No JSON returned from HTTP request"_ this might be due to a mismatch between the Webhook ID and the device settings on the HomeAssistant server. We have discovered that the Webhook ID is required for HomeAssistant API calls with templates in order to work in a non-privileged account. The application options include the ability to clear the Webhook ID in the application forcing a new one to be set up. This should prevent the above error being shown on startup.
Look for this option in the application settings:
![HTTP 410](images/http_410_error.jpg)
We now also have reports of an [HTTP 410](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/410) error occurring after an application update. With thanks to [@Aaroneisele55](https://github.com/Aaroneisele55) for resolving this issue also by the clearing of the Webhook ID. The cause of the problem remains unknown as updates do not generally require this correction between the Home Assistant server and the watch settings.
**Therefore, when the URL is known to work, any failure to return the JSON menu definition from an HTTPS request should try resetting the Webhook ID used with Home Assistant.**
To reset the Webhook ID look for this option in the application settings:
![Nabu Casa Setup](images/delete_webhook_id.png)

View File

@@ -1,4 +1,4 @@
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Numeric](examples/Numeric.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [HTTP Headers](HTTP_Headers.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
# Wi-Fi & LTE

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Numeric](Numeric.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
# Actions
@@ -37,6 +37,14 @@ For example:
"confirm": true
}
}
```
The `confirm` field may contain a string instead of a Boolean in order to provide a custom message to display instead of the default "Sure?" text.
```json
"tap_action": {
"confirm": "Toggle the cover?"
}
```
**The authors do not advise the use of this application for security sensitive devices. But we suspect users are taking that risk anyway, hence a PIN confirmation is provided that can be used for additional menu item security.**
@@ -87,9 +95,9 @@ You can choose individual items that will quit after they have completed their a
"name": "Turn off Stuff",
"type": "tap",
"tap_action": {
"action": "automation.trigger"
"action": "automation.trigger",
"exit": true
},
"exit": true
}
```
@@ -108,3 +116,64 @@ If you would like to temporarily disable an item in your menu, e.g. for seasonal
"enabled": false
}
```
# Selects
Here is an example of how to make a light effect selector:
```json
{
"type": "group",
"name": "Example",
"title": "Light Effect",
"content": "{{ state_attr('light.moon', 'effect') }}",
"items": [
{
"type": "tap",
"name": "None",
"entity": "light.example",
"tap_action": {
"service": "light.turn_on",
"data": {
"effect": "None"
}
}
},
{
"type": "tap",
"name": "Rainbow",
"entity": "light.example",
"tap_action": {
"service": "light.turn_on",
"data": {
"effect": "Rainbow"
}
}
},
{
"type": "tap",
"name": "Glimmer",
"entity": "light.example",
"tap_action": {
"service": "light.turn_on",
"data": {
"effect": "Glimmer"
}
}
},
{
"type": "tap",
"name": "Twinkle",
"entity": "light.example",
"tap_action": {
"service": "light.turn_on",
"data": {
"effect": "Twinkle"
}
}
}
]
}
```
The same pattern works for any selector (`input_select.*`, `select.*`, `climate.*` mode).

View File

@@ -1,4 +1,4 @@
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Numeric](Numeric.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
# Glance
@@ -75,6 +75,6 @@ The following shows the default glance when the menu file is not available at th
<img src="../images/Venu2_glance_no_menu.png" width="200" title="Venu 2 Glance showing errors"/>
Once the custom glance template has been retrieved and evaluated the display will change. Should the connectivity to your Home Assistant then be lost, e.g. you move out of range of your phone, the glance reflects this in the colour of the residual two rectangles. The top one remains an indicator for the API, and the bottom rectangle remains an indicator for the menu availability, reflecting the original placement in the default glance view that has now been replaced.
Once the custom glance template has been retrieved and evaluated the display will change. Should the connectivity to your HomeAssistant then be lost, e.g. you move out of range of your phone, the glance reflects this in the colour of the residual two rectangles. The top one remains an indicator for the API, and the bottom rectangle remains an indicator for the menu availability, reflecting the original placement in the default glance view that has now been replaced.
<img src="../images/Venu2_glance_no_bt.png" width="200" title="Venu 2 Glance showing lost connectivity"/>

167
examples/Numeric.md Normal file
View File

@@ -0,0 +1,167 @@
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Numeric](Numeric.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
# Numeric
Provides a number picker in order to adjust a numeric value of an entity.
## Thermostat
An example using a thermostat as a `numeric` menu item.
```json
{
"name": "Heating",
"content": "{{ ' %.1f' | format(state_attr('climate.room','temperature')) }}",
"type": "numeric",
"entity": "climate.room",
"tap_action": {
"action": "climate.set_temperature",
"picker": {
"step": 0.5,
"min": 10,
"max": 30,
"attribute": "temperature",
"data_attribute": "temperature"
}
}
}
```
This needs some explanation. The `tap_action` object needs a `picker` object to specify the numeric menu item's behaviour. The `picker` object is described in the table below.
Field | Purpose | Mandatory |
-----------------|----------------------------------------------------------------|-----------|
`step` | The increment or decrement step size. | Yes |
`min` | The minimum value the numeric entity can take. | Yes |
`max` | The maximum value the numeric entity can take. | Yes |
`attribute` | The attribute on the `entity` that holds the state to be read. | No |
`data_attribute` | The attribute on the `action` call that sets the state. | Yes |
It may well be the case that often `attribute` and `data_attribute` are the same attribute, as with this example.
## Helper
You might define a "helper" entity as follows in HomeAssistant:
<img src="../images/my_float.png" width="400" title="HomeAssistant Helper definition for an 'input_number'." style="margin:5px"/>
In this case, the state is the actual value, so the template uses `states(..)` instead of `state_attr(..)`, you must not set the optional `attribute` value in the JSON definition so that the application uses the correct template internally for querying the HA server for its present value. Your own template definition in the `content` field will need to follow suit too. The `data_attribute` must be set to `value` for the `action` call that sets the chosen value from the number carousel.
```json
{
"name": "My Float",
"content": "Currently {{ states('input_number.my_float') }}",
"type": "numeric",
"entity": "input_number.my_float",
"tap_action": {
"action": "input_number.set_value",
"picker": {
"step": 0.5,
"min": -10.0,
"max": 10.0,
"data_attribute": "value"
}
}
}
```
## Amplifier
The complication here is this amplifier uses one scale for changing the value, a range 0.0 to 1.0, and another to render the volume on the display, dB. So the template does some scale conversion, but the number picker has to use the 0.0 to 1.0 range which is annoying.
```json
{
"name": "Amplifer Volume",
"content": "{{ '%.1f' | format(state_attr('media_player.amplifier','volume_level') * 100 -80) }} dB ({{ state_attr('media_player.amplifier','volume_level') }})",
"type": "numeric",
"entity": "media_player.amplifier",
"tap_action": {
"action": "media_player.volume_set",
"picker": {
"step": 0.005,
"min": 0.2,
"max": 0.6,
"attribute": "volume_level",
"data_attribute": "volume_level"
}
}
}
```
<img src="../images/number_picker_raw.bmp" width="200" title="HomeAssistant Helper definition for an 'input_number'." style="margin:5px"/>
The above is a little awkward to change the volume as the picker's scale is unfamiliar. To make life easier you might choose to implement a "Template number" in HomeAssistant as defined in the following dialogue box.
<img src="../images/template_number.png" width="500" title="HomeAssistant Helper definition for an 'input_number'." style="margin:5px"/>
For copy and paste, the Jinja2 fields are as follows:
1. Template rendering with conversion to dB:
```jinja
{{ state_attr('media_player.amplifier','volume_level') * 100 -80 }}
```
2. Conversion from dB to range 0.0 to 1.0:
```jinja
{{ (value+80)/100 }}
```
3. Availability template:
```jinja
{{ not is_state('media_player.amplifier','unavailable') }}
```
<img src="../images/number_picker_db.bmp" width="200" title="HomeAssistant Helper definition for an 'input_number'." style="margin:5px"/>
As an alternative to using the GUI, the following can be pasted into HomeAssistant's `configuration.yaml`:
```yaml
template:
- number:
- name: "Amplifier dB"
unique_id: "<Generate Unique ID>"
unit_of_measurement: "dB"
state: "{{ state_attr('media_player.amplifier','volume_level') * 100 -80 }}"
availability: "{{ not is_state('media_player.amplifier','unavailable') }}"
set_value:
- action: media_player.volume_set
target:
entity_id: media_player.amplifier
data:
volume_level: "{{ (value+80)/100 }}"
step: 0.5
min: -60
max: -15
icon: mdi:audio-video
```
We noticed some schema checking errors when we tried this, but the YAML above is consistent with the HA [Template](https://www.home-assistant.io/integrations/template/#number) support pages, and this code does correctly create the number template as required.
The JSON menu definition can now use dB with the new template number as follows.
```json
{
"name": "Amplifier Volume",
"content": "{% if is_state('media_player.amplifier','unavailable') %}Off{% else %}{{ '%.1f' | format(states('number.amplifier_db') | float) }} dB{% endif %}",
"type": "numeric",
"entity": "number.amplifier_db",
"tap_action": {
"action": "number.set_value",
"picker": {
"step": 0.5,
"min": -60.0,
"max": -15.0,
"data_attribute": "value"
}
}
},
```
## Trouble Shooting
Specific to this menu item:
1. If the number picker does not initialise with the correct value, amend the `attribute` field. Just because your template renders does not mean the application has extracted the numeric value as the `content` template is rendered on the HomeAssistant server.

View File

@@ -1,4 +1,4 @@
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Numeric](Numeric.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
# Switches
@@ -118,7 +118,9 @@ You can choose individual items that will quit after they have completed their a
"entity": "light.hall_light",
"name": "Hall Light & Quit",
"type": "toggle",
"exit": true
"tap_action" {
"exit": true
}
}
```

View File

@@ -1,4 +1,4 @@
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Numeric](Numeric.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [HTTP Headers](../HTTP_Headers.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
# Templates

View File

@@ -44,15 +44,22 @@ Half = 2
# Original icons for 416x416 screen size with 48x48 icons
original = (96, 48, 24)
# 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 19 18
# Screen 480 454 416 390 360 320 280 260 240 218 208 176 166 156
# Convert icons to different screen sizes by these parameters
lookup = {
# Doub Sing Half
# 0 1 2
480: (110, 55, 28),
454: (106, 53, 27),
# 416: ( 96, 48, 24),
390: ( 90, 46, 23),
360: ( 84, 42, 21),
320: ( 74, 38, 19),
295: ( 68, 34, 17), # Especially for the instinct3amoled50mm device that clip the icons
280: ( 64, 32, 16),
260: ( 60, 30, 15),
240: ( 56, 28, 14),

BIN
images/http_410_error.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/my_float.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
images/number_picker_db.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

BIN
images/template_number.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -40,7 +40,7 @@ output_dir_prefix = 'resources-launcher-'
input_dir = output_dir_prefix + '70-70'
# Convert icons to different screen sizes by these parameters
lookup = [26, 30, 33, 35, 36, 40, 54, 60, 61, 62, 65, 80]
lookup = [26, 30, 33, 35, 36, 38, 40, 52, 54, 56, 60, 61, 62, 65, 68, 80]
# Delete all but the original 48x48 icon directories
for entry in os.listdir("."):

View File

@@ -24,7 +24,7 @@
Use "Monkey C: Edit Application" from the Visual Studio Code command palette
to update the application attributes.
-->
<iq:application id="971834c4-e4fc-4825-801f-7ac9db0e3044" type="watch-app" name="@Strings.AppName" entry="HomeAssistantApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="3.1.0">
<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 command palette to edit
the build targets:

View File

@@ -143,7 +143,7 @@ fenix8solar51mm.resourcePath = $(fenix8solar51mm.resourcePath);resources-launche
fenixchronos.resourcePath = $(fenixchronos.resourcePath);resources-launcher-36-36;resources-icons-26
# Screen Size 416x416 launcher icon size 60x60
fenixe.resourcePath = $(fenixe.resourcePath);resources-launcher-60-60;resources-icons-48
# Screen Size 390 x 390 launcher icon size 54x54
# Screen Size 390x390 launcher icon size 54x54
fr165.resourcePath = $(descentmk2s.resourcePath);resources-launcher-54-54;resources-icons-46
fr165m.resourcePath = $(descentmk2s.resourcePath);resources-launcher-54-54;resources-icons-46
# Screen Size 240x240 launcher icon size 40x40

View File

@@ -133,20 +133,20 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
var data = { "gps_accuracy" => accuracy };
// Only add the non-null fields as all the values are optional in Home Assistant, and it avoid submitting fake values.
if (position.position != null) {
data.put("gps", position.position.toDegrees());
data["gps"] = position.position.toDegrees();
}
if (position.speed != null) {
data.put("speed", Math.round(position.speed));
data["speed"] = Math.round(position.speed);
}
if (position.heading != null) {
var heading = Math.round(position.heading * 180 / Math.PI);
while (heading < 0) {
heading += 360;
}
data.put("course", heading);
data["course"] = heading;
}
if (position.altitude != null) {
data.put("altitude", Math.round(position.altitude));
data["altitude"] = Math.round(position.altitude);
}
// System.println("BackgroundServiceDelegate onTemporalEvent(): data = " + data.toString());

View File

@@ -629,6 +629,12 @@ class HomeAssistantApp extends Application.AppBase {
if (item instanceof HomeAssistantToggleMenuItem) {
(item as HomeAssistantToggleMenuItem).updateToggleState(data[i.toString() + "t"]);
}
if (item instanceof HomeAssistantNumericMenuItem) {
var s = data[i.toString() + "n"];
if ((s instanceof Lang.Number) or (s instanceof Lang.Float)) {
(item as HomeAssistantNumericMenuItem).setValue(s);
}
}
}
if (Settings.getMenuCheck() && Settings.getCacheConfig() && !mIsCacheChecked) {
// We are caching the menu configuration, so let's fetch it and check if its been updated.
@@ -714,14 +720,13 @@ class HomeAssistantApp extends Application.AppBase {
var item = mItemsToUpdate[i];
var template = item.getTemplate();
if (template != null) {
mTemplates.put(i.toString(), {
"template" => template
});
mTemplates[i.toString()] = { "template" => template };
}
if (item instanceof HomeAssistantToggleMenuItem) {
mTemplates.put(i.toString() + "t", {
"template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate()
});
mTemplates[i.toString() + "t"] = { "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate() };
}
if (item instanceof HomeAssistantNumericMenuItem) {
mTemplates[i.toString() + "n"] = { "template" => (item as HomeAssistantNumericMenuItem).getNumericTemplate() };
}
}
}
@@ -822,7 +827,7 @@ class HomeAssistantApp extends Application.AppBase {
var phoneConnected = System.getDeviceSettings().phoneConnected;
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
// System.println("API URL = " + Settings.getApiUrl());
// System.println("HomeAssistantApp fetchApiStatus(): API URL = " + Settings.getApiUrl());
if (Settings.getApiUrl().equals("")) {
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
WatchUi.requestUpdate();

View File

@@ -24,10 +24,13 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation {
//! Class Constructor
//
function initialize() {
WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String);
function initialize(message as Lang.String?) {
if (message == null) {
WatchUi.Confirmation.initialize(WatchUi.loadResource($.Rez.Strings.Confirm) as Lang.String);
} else {
WatchUi.Confirmation.initialize(message);
}
}
}
//! Delegate to respond to the confirmation request.

View File

@@ -33,7 +33,7 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
}?
) {
if (options != null) {
options.put(:icon, icon);
options[:icon] = icon;
} else {
options = { :icon => icon };
}

View File

@@ -31,9 +31,7 @@ class HomeAssistantMenuItemFactory {
//! Class Constructor
//
private function initialize() {
mMenuItemOptions = {
:alignment => Settings.getMenuAlignment()
};
mMenuItemOptions = { :alignment => Settings.getMenuAlignment() };
mTapTypeIcon = new WatchUi.Bitmap({
:rezId => $.Rez.Drawables.TapTypeIcon,
@@ -84,7 +82,7 @@ class HomeAssistantMenuItemFactory {
) as WatchUi.MenuItem {
var keys = mMenuItemOptions.keys();
for (var i = 0; i < keys.size(); i++) {
options.put(keys[i], mMenuItemOptions.get(keys[i]));
options[keys[i]] = mMenuItemOptions.get(keys[i]);
}
return new HomeAssistantToggleMenuItem(
label,
@@ -119,12 +117,12 @@ class HomeAssistantMenuItemFactory {
if (data == null) {
data = { "entity_id" => entity_id };
} else {
data.put("entity_id", entity_id);
data["entity_id"] = entity_id;
}
}
var keys = mMenuItemOptions.keys();
for (var i = 0; i < keys.size(); i++) {
options.put(keys[i], mMenuItemOptions.get(keys[i]));
options[keys[i]] = mMenuItemOptions.get(keys[i]);
}
if (action != null) {
options.put(:icon, mTapTypeIcon);
@@ -137,7 +135,7 @@ class HomeAssistantMenuItemFactory {
mHomeAssistantService
);
} else {
options.put(:icon, mInfoTypeIcon);
options[:icon] = mInfoTypeIcon;
return new HomeAssistantTapMenuItem(
label,
template,
@@ -157,8 +155,8 @@ class HomeAssistantMenuItemFactory {
label as Lang.String or Lang.Symbol,
entity_id as Lang.String?,
template as Lang.String?,
action as Lang.String?,
data as Lang.Dictionary?,
action as Lang.String?,
picker as Lang.Dictionary,
options as {
:exit as Lang.Boolean,
:confirm as Lang.Boolean,
@@ -166,25 +164,21 @@ class HomeAssistantMenuItemFactory {
:icon as WatchUi.Bitmap
}
) as WatchUi.MenuItem {
var data = null;
if (entity_id != null) {
if (data == null) {
data = { "entity_id" => entity_id };
} else {
data.put("entity_id", entity_id);
}
data = { "entity_id" => entity_id };
}
var keys = mMenuItemOptions.keys();
for (var i = 0; i < keys.size(); i++) {
options.put(keys[i], mMenuItemOptions.get(keys[i]));
options[keys[i]] = mMenuItemOptions.get(keys[i]);
}
options.put(:icon, mTapTypeIcon);
options[:icon] = mTapTypeIcon;
return new HomeAssistantNumericMenuItem(
label,
template,
action,
data,
picker,
options,
mHomeAssistantService
);

View File

@@ -9,80 +9,92 @@
// 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
// P A Abbey & J D Abbey & @thmichel, 13 October 2025
//
//------------------------------------------------------------
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.Lang;
using Toybox.WatchUi;
//! Factory that controls which numbers can be picked
class HomeAssistantNumericFactory extends WatchUi.PickerFactory {
// define default values in case not contained in data
private var mStart as Lang.Float = 0.0;
private var mStop as Lang.Float = 100.0;
private var mStep as Lang.Float = 1.0;
private var mFormatString as Lang.String = "%.2f";
private var mStart as Lang.Float = 0.0;
private var mStop as Lang.Float = 100.0;
private var mStep as Lang.Float = 1.0;
private var mFormatString as Lang.String = "%d";
//! Class Constructor
//!
public function initialize(data as Lang.Dictionary) {
//
public function initialize(picker as Lang.Dictionary) {
PickerFactory.initialize();
// Get values from data
var val = data.get("start");
var val = picker["min"];
if (val != null) {
mStart = val.toString().toFloat();
}
val = data.get("stop");
val = picker["max"];
if (val != null) {
mStop = val.toString().toFloat();
}
val = data.get("step");
val = picker["step"];
if (val != null) {
mStep = val.toString().toFloat();
}
val = data.get("formatString");
if (val != null) {
mFormatString = val.toString();
if (mStep > 0.0) {
var s = mStep;
var dp = 0;
while (s < 1.0) {
s *= 10;
dp++;
// Assigned inside the loop and in each iteration to avoid clobbering the default '%d'.
mFormatString = "%." + dp.toString() + "f";
}
} else {
// The JSON menu definition defined a step size of 0, revert to the default.
mStep = 1.0;
}
}
//! Get the index of a number item
//! @param value The number to get the index of
//! @return The index of the number
public function getIndex(value as Float) as Number {
return ((value / mStep) - mStart).toNumber();
}
//! Generate a Drawable instance for an item
//!
//! @param index The item index
//! @param selected true if the current item is selected, false otherwise
//! @return Drawable for the item
public function getDrawable(index as Number, selected as Boolean) as Drawable? {
//
public function getDrawable(
index as Lang.Number,
selected as Lang.Boolean
) as WatchUi.Drawable? {
var value = getValue(index);
var text = "No item";
if (value instanceof Lang.Float) {
text = value.format(mFormatString);
}
return new WatchUi.Text({:text=>text, :color=>Graphics.COLOR_WHITE,
:locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_CENTER});
return new WatchUi.Text({
:text => text,
:color => Graphics.COLOR_WHITE,
:locX => WatchUi.LAYOUT_HALIGN_CENTER,
:locY => WatchUi.LAYOUT_VALIGN_CENTER
});
}
//! Get the value of the item at the given index
//!
//! @param index Index of the item to get the value of
//! @return Value of the item
public function getValue(index as Number) as Object? {
//
public function getValue(index as Lang.Number) as Lang.Object? {
return mStart + (index * mStep);
}
//! Get the number of picker items
//!
//! @return Number of items
public function getSize() as Number {
//
public function getSize() as Lang.Number {
return ((mStop - mStart) / mStep).toNumber() + 1;
}
}

View File

@@ -9,7 +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
// P A Abbey & J D Abbey & @thmichel, 13 October 2025
//
//-----------------------------------------------------------------------------------
@@ -17,26 +17,25 @@ using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
//! a Home Assistant Template.
//
class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService?;
private var mAction as Lang.String?;
private var mConfirm as Lang.Boolean;
private var mConfirm as Lang.Boolean or Lang.String or Null;
private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary?;
private var mValue as Lang.String?;
private var mFormatString as Lang.String="%.1f";
private var mPicker as Lang.Dictionary?;
private var mValue as Lang.Number or Lang.Float = 0;
private var mFormatString as Lang.String = "%d";
//! Class Constructor
//!
//! @param label Menu item label.
//! @param template Menu item template.
//! @param action Menu item action.
//! @param action Menu item action.
//! @param data Data to supply to the action call.
//! @param exit Should the action call complete and then exit?
//! @param confirm Should the action call be confirmed to avoid accidental invocation?
@@ -49,8 +48,9 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
action as Lang.String?,
action as Lang.String?,
data as Lang.Dictionary?,
picker as Lang.Dictionary,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
@@ -62,6 +62,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
) {
mAction = action;
mData = data;
mPicker = picker;
mExit = options[:exit];
mConfirm = options[:confirm];
mPin = options[:pin];
@@ -76,8 +77,21 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
:icon => options[:icon]
}
);
}
if (picker != null) {
var s = picker["step"];
if (s != null) {
var step = s.toFloat() as Lang.Float;
var dp = 0;
while (step < 1.0) {
step *= 10;
dp++;
// Assigned inside the loop and in each iteration to avoid clobbering the default '%d'.
mFormatString = "%." + dp.toString() + "f";
}
}
}
}
function callAction() as Void {
@@ -89,10 +103,10 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView(
pinConfirmationView,
new HomeAssistantPinConfirmationDelegate({
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
}),
WatchUi.SLIDE_IMMEDIATE
);
@@ -106,15 +120,20 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView(
dialog,
new WifiLteExecutionConfirmDelegate({
:type => "action",
:type => "action",
:action => mAction,
:data => mData,
:exit => mExit,
:data => mData,
:exit => mExit,
}, dialog),
WatchUi.SLIDE_LEFT
);
} else {
var view = new HomeAssistantConfirmation();
var view;
if (mConfirm instanceof Lang.String) {
view = new HomeAssistantConfirmation(mConfirm as Lang.String?);
} else {
view = new HomeAssistantConfirmation(null);
}
WatchUi.pushView(
view,
new HomeAssistantConfirmationDelegate({
@@ -130,25 +149,57 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
}
}
//! 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 {
mHomeAssistantService.call(mAction, {"entity_id" => mData.get("entity_id").toString(),mData.get("valueLabel").toString() => mValue}, mExit);
var dataAttribute = mPicker["data_attribute"] as Lang.String?;
var entity_id = mData["entity_id"] as Lang.String?;
WatchUi.popView(WatchUi.SLIDE_RIGHT);
WatchUi.requestUpdate();
if (dataAttribute == null or entity_id == null) {
// Return without service call if no data attribute or entity ID is set to avoid crash.
return;
}
if (mAction != null) {
mHomeAssistantService.call(
mAction,
{
"entity_id" => entity_id.toString(),
dataAttribute.toString() => mValue
},
mExit
);
}
}
//! Return a numeric menu item's fetch state template.
//!
//! @return A string with the menu item's template definition (or null).
//
function getNumericTemplate() as Lang.String? {
var entity_id = mData["entity_id"] as Lang.String?;
var attribute = mPicker["attribute"] as Lang.String?;
if (entity_id == null) {
return null;
} else {
if (attribute == null) {
return "{{states('" + entity_id.toString() + "')}}";
} else {
return "{{state_attr('" + entity_id.toString() + "','" + attribute + "')}}";
}
}
}
//! 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 {
public 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.Float) {
@@ -157,26 +208,45 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
} else if(data instanceof Lang.Number) {
var f = data.toFloat() as Lang.Float;
setSubLabel(f.format(mFormatString));
} else if (data instanceof Lang.String){
} else if (data instanceof Lang.String) {
// This should not happen
setSubLabel(data);
}
else {
// The template must return a Float, or the item cannot be formatted locally without error.
} else {
// The template must return a Float on Numeric value, or the item cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
//! Set the mValue value.
//! Set the Picker's value. Needed to set new value via the Action call
//!
//! Needed to set new value via the Service call
//! @param value New value to set.
//
function setValue(value as Lang.String) as Void {
public function setValue(value as Lang.Number or Lang.Float) as Void {
mValue = value;
}
function getData() as Lang.Dictionary {
//! Get the Picker's value.
//!
//! Needed to set new value via the Action call
//
public function getValue() as Lang.Number or Lang.Float {
return mValue;
}
//! Get the original 'data' field supplied by the JSON menu.
//!
//! @return Dictionary containing the 'data' field.
//
public function getData() as Lang.Dictionary {
return mData;
}
// Get the original 'picker' field supplied by the JSON menu.
//!
//! @return Dictionary containing the 'picker' field.
//
public function getPicker() as Lang.Dictionary {
return mPicker;
}
}

View File

@@ -9,7 +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
// P A Abbey & J D Abbey & @thmichel, 13 October 2025
//
//------------------------------------------------------------
@@ -20,83 +20,79 @@ using Toybox.System;
using Toybox.WatchUi;
//! Picker that allows the user to choose a float value
//
class HomeAssistantNumericPicker extends WatchUi.Picker {
private var mFactory as HomeAssistantNumericFactory;
private var mItem as HomeAssistantNumericMenuItem;
//! Constructor
public function initialize(factory as HomeAssistantNumericFactory, haItem as HomeAssistantNumericMenuItem) {
//
public function initialize(
factory as HomeAssistantNumericFactory,
haItem as HomeAssistantNumericMenuItem
) {
mItem = haItem;
var picker = mItem.getPicker();
var min = (picker.get("min") as Lang.String).toFloat();
var step = (picker.get("step") as Lang.String).toFloat();
var val = haItem.getValue();
mFactory = factory;
var pickerOptions = {:pattern=>[mFactory]};
mItem=haItem;
var data = mItem.getData();
var start = 0.0;
var val = data.get("start");
if (val != null) {
start = val.toString().toFloat();
if (min == null) {
min = 0.0;
}
var step = 1.0;
val = data.get("step");
if (val != null) {
step = val.toString().toFloat();
if (step == null) {
step = 1.0;
}
val = haItem.getSubLabel().toFloat();
var index = ((val -start) / step).toNumber();
pickerOptions[:defaults] =[index];
var title = new WatchUi.Text({:text=>haItem.getLabel(), :locX=>WatchUi.LAYOUT_HALIGN_CENTER,
:locY=>WatchUi.LAYOUT_VALIGN_BOTTOM});
pickerOptions[:title] = title;
Picker.initialize(pickerOptions);
WatchUi.Picker.initialize({
:title => new WatchUi.Text({
:text => haItem.getLabel(),
:locX => WatchUi.LAYOUT_HALIGN_CENTER,
:locY => WatchUi.LAYOUT_VALIGN_BOTTOM
}),
:pattern => [factory],
:defaults => [((val - min) / step).toNumber()]
});
}
//! Get whether the user is done picking
//! Called when the user has completed picking.
//!
//! @param value Value user selected
//! @return true if user is done, false otherwise
public function onConfirm(value as Lang.String) as Void {
//
public function onConfirm(value as Lang.Number or Lang.Float) as Void {
mItem.setValue(value);
mItem.callAction();
}
}
//! Responds to a numeric picker selection or cancellation
//! Responds to a numeric picker selection or cancellation.
//
class HomeAssistantNumericPickerDelegate extends WatchUi.PickerDelegate {
private var mPicker as HomeAssistantNumericPicker;
//! Constructor
//
public function initialize(picker as HomeAssistantNumericPicker) {
PickerDelegate.initialize();
mPicker = picker;
}
//! Handle a cancel event from the picker
//!
//! @return true if handled, false otherwise
//
public function onCancel() as Lang.Boolean {
WatchUi.popView(WatchUi.SLIDE_RIGHT);
return true;
}
//! Handle a confirm event from the picker
//!
//! @param values The values chosen in the picker
//! @return true if handled, false otherwise
//
public function onAccept(values as Lang.Array) as Lang.Boolean {
var chosenValue = values[0].toString();
mPicker.onConfirm(chosenValue);
WatchUi.popView(WatchUi.SLIDE_RIGHT);
mPicker.onConfirm(values[0]);
return true;
}
}

View File

@@ -34,7 +34,7 @@ class HomeAssistantService {
}
}
//! Callback function after completing the POST request to call a service.
//! Callback function after completing the POST request to call an action.
//!
//! @param responseCode Response code.
//! @param data Response data.
@@ -121,7 +121,7 @@ class HomeAssistantService {
//! Invoke a action call for a menu item.
//!
//! @param action The Home Assistant action to be run, e.g. from the JSON `action` field.
//! @param data Data to be supplied to the action call.
//! @param data Data to be supplied to the action call.
//
function call(
action as Lang.String,
@@ -136,10 +136,10 @@ class HomeAssistantService {
WatchUi.pushView(
dialog,
new WifiLteExecutionConfirmDelegate({
:type => "action",
:type => "action",
:action => action,
:data => data,
:exit => exit,
:data => data,
:exit => exit,
}, dialog),
WatchUi.SLIDE_LEFT
);

View File

@@ -17,12 +17,12 @@ using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
//! Menu button that triggers a service.
//! Menu button that triggers an action.
//
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mAction as Lang.String?;
private var mConfirm as Lang.Boolean;
private var mConfirm as Lang.Boolean or Lang.String or Null;
private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean;
private var mData as Lang.Dictionary?;
@@ -31,7 +31,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//!
//! @param label Menu item label.
//! @param template Menu item template.
//! @param action Menu item action.
//! @param action Menu item action.
//! @param data Data to supply to the action call.
//! @param exit Should the action call complete and then exit?
//! @param confirm Should the action call be confirmed to avoid accidental invocation?
@@ -44,7 +44,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
action as Lang.String?,
action as Lang.String?,
data as Lang.Dictionary?,
options as {
:alignment as WatchUi.MenuItem.Alignment,
@@ -83,10 +83,10 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView(
pinConfirmationView,
new HomeAssistantPinConfirmationDelegate({
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
:callback => method(:onConfirm),
:pin => pin,
:state => false,
:view => pinConfirmationView,
}),
WatchUi.SLIDE_IMMEDIATE
);
@@ -95,8 +95,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
if ((! System.getDeviceSettings().phoneConnected ||
! System.getDeviceSettings().connectionAvailable) &&
Settings.getWifiLteExecutionEnabled()) {
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
var dialog = new WatchUi.Confirmation(dialogMsg);
var dialog = new WatchUi.Confirmation(WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String);
WatchUi.pushView(
dialog,
new WifiLteExecutionConfirmDelegate({
@@ -108,7 +107,12 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
WatchUi.SLIDE_LEFT
);
} else {
var view = new HomeAssistantConfirmation();
var view;
if (mConfirm instanceof Lang.String) {
view = new HomeAssistantConfirmation(mConfirm as Lang.String?);
} else {
view = new HomeAssistantConfirmation(null);
}
WatchUi.pushView(
view,
new HomeAssistantConfirmationDelegate({
@@ -128,7 +132,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//!
//! @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 {
public function onConfirm(b as Lang.Boolean) as Void {
if (mAction != null) {
mHomeAssistantService.call(mAction, mData, mExit);
}

View File

@@ -25,7 +25,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mData as Lang.Dictionary;
private var mTemplate as Lang.String?;
private var mExit as Lang.Boolean;
private var mConfirm as Lang.Boolean;
private var mConfirm as Lang.Boolean or Lang.String or Null;
private var mPin as Lang.Boolean;
private var mHasVibrate as Lang.Boolean = false;
@@ -72,12 +72,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
//
private function setUiToggle(state as Null or Lang.String) as Void {
if (state != null) {
if (state.equals("unavailable" || "unknown")) {
return;
} else if ((state.equals("off") || state.equals("closed")) && isEnabled()) {
setEnabled(false);
} else if (!isEnabled()) {
if (state.equals("on") && !isEnabled()) {
setEnabled(true);
} else if (state.equals("off") && isEnabled()) {
setEnabled(false);
}
}
}
@@ -222,7 +220,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
}
//! Handles the response from a Home Assistant service or state call and updates the toggle UI.
//! Handles the response from a Home Assistant action or state call and updates the toggle UI.
//!
//! @param data An array of dictionaries, each representing a Home Assistant entity state.
//
@@ -295,7 +293,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
}
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
//! Call a Home Assistant action only after checks have been done for confirmation or PIN entry.
//
function callAction(b as Lang.Boolean) as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
@@ -326,7 +324,12 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
Settings.getWifiLteExecutionEnabled()) {
wifiPrompt(b);
} else {
var confirmationView = new HomeAssistantConfirmation();
var confirmationView;
if (mConfirm instanceof Lang.String) {
confirmationView = new HomeAssistantConfirmation(mConfirm as Lang.String?);
} else {
confirmationView = new HomeAssistantConfirmation(null);
}
WatchUi.pushView(
confirmationView,
new HomeAssistantConfirmationDelegate({
@@ -351,7 +354,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
setState(b);
}
//! Displays a confirmation dialog before executing a service call via Wi-Fi/LTE.
//! Displays a confirmation dialog before executing an action call via Wi-Fi/LTE.
//!
//! @param s Desired state: `true` to turn on, `false` to turn off.
//

View File

@@ -35,7 +35,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
if (options == null) {
options = { :title => definition.get("title") as Lang.String };
} else {
options.put(:title, definition.get("title") as Lang.String);
options[:title] = definition.get("title") as Lang.String;
}
WatchUi.Menu2.initialize(options);
@@ -47,8 +47,8 @@ class HomeAssistantView extends WatchUi.Menu2 {
var content = items[i].get("content") as Lang.String?;
var entity = items[i].get("entity") as Lang.String?;
var tap_action = items[i].get("tap_action") as Lang.Dictionary?;
var action = items[i].get("service") as Lang.String?; // Deprecated schema
var confirm = false as Lang.Boolean?;
var action = items[i].get("service") as Lang.String?; // Deprecated schema
var confirm = false as Lang.Boolean or Lang.String or Null;
var pin = false as Lang.Boolean?;
var data = null as Lang.Dictionary?;
var enabled = true as Lang.Boolean?;
@@ -57,12 +57,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
enabled = items[i].get("enabled"); // Optional
}
if (items[i].get("exit") != null) {
exit = items[i].get("exit"); // Optional
exit = items[i].get("exit"); // Deprecated
}
if (tap_action != null) {
action = tap_action.get("service"); // Deprecated
action = tap_action.get("service"); // Deprecated
if (tap_action.get("action") != null) {
action = tap_action.get("action"); // Optional
action = tap_action.get("action"); // Optional
}
data = tap_action.get("data"); // Optional
if (tap_action.get("confirm") != null) {
@@ -71,6 +71,9 @@ class HomeAssistantView extends WatchUi.Menu2 {
if (tap_action.get("pin") != null) {
pin = tap_action.get("pin"); // Optional
}
if (tap_action.get("exit") != null) {
exit = tap_action.get("exit"); // Optional
}
}
if (type != null && name != null && enabled) {
if (type.equals("toggle") && entity != null) {
@@ -130,18 +133,23 @@ class HomeAssistantView extends WatchUi.Menu2 {
));
}
} else if (type.equals("numeric") && action != null) {
addItem(HomeAssistantMenuItemFactory.create().numeric(
name,
entity,
content,
action,
data,
{
:exit => exit,
:confirm => confirm,
:pin => pin
if (tap_action != null) {
var picker = tap_action.get("picker") as Lang.Dictionary?;
if (picker != null) {
addItem(HomeAssistantMenuItemFactory.create().numeric(
name,
entity,
content,
action,
picker,
{
:exit => exit,
:confirm => confirm,
:pin => pin
}
));
}
));
}
} else if (type.equals("info") && content != null) {
// Cannot exit from a non-actionable information only menu item.
addItem(HomeAssistantMenuItemFactory.create().tap(
@@ -170,7 +178,6 @@ class HomeAssistantView extends WatchUi.Menu2 {
//!
//! @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 HomeAssistantNumericMenuItem or Null> {
var fullList = [];
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
@@ -206,8 +213,8 @@ class HomeAssistantView extends WatchUi.Menu2 {
//! 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 {}
}
//! Delegate for the HomeAssistantView.
@@ -275,18 +282,13 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
var haItem = item as HomeAssistantNumericMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId());
// create new view to select new value
var mPickerFactory = new HomeAssistantNumericFactory(haItem.getData());
var mPicker = new HomeAssistantNumericPicker(mPickerFactory,haItem);
var mPickerFactory = new HomeAssistantNumericFactory((haItem as HomeAssistantNumericMenuItem).getPicker());
var mPicker = new HomeAssistantNumericPicker(mPickerFactory,haItem);
var mPickerDelegate = new HomeAssistantNumericPickerDelegate(mPicker);
WatchUi.pushView(mPicker,mPickerDelegate,WatchUi.SLIDE_LEFT);
} else if (item instanceof HomeAssistantGroupMenuItem) {
var haMenuItem = item as HomeAssistantGroupMenuItem;
// System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId());
WatchUi.pushView(haMenuItem.getMenuView(), new HomeAssistantViewDelegate(false), WatchUi.SLIDE_LEFT);
// } else {
// System.println(item.getLabel() + " " + item.getId());
}
}

View File

@@ -24,12 +24,12 @@ using Toybox.Timer;
//
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
public static var mCommandData as {
:type as Lang.String,
:type as Lang.String,
:action as Lang.String?,
:data as Lang.Dictionary?,
:url as Lang.String?,
:id as Lang.Number?,
:exit as Lang.Boolean
:data as Lang.Dictionary?,
:url as Lang.String?,
:id as Lang.Number?,
:exit as Lang.Boolean
}?;
private static var mTimer as Timer.Timer?;
@@ -52,7 +52,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
function initialize(
cOptions as {
:type as Lang.String,
:action as Lang.String?,
:action as Lang.String?,
:data as Lang.Dictionary?,
:url as Lang.String?,
:callback as Lang.Method?,
@@ -73,7 +73,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
mConfirmationView = view;
mCommandData = {
:type => cOptions[:type],
:action => cOptions[:action],
:action => cOptions[:action],
:data => cOptions[:data],
:url => cOptions[:url],
:callback => cOptions[:callback],

View File

@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -7,7 +8,7 @@
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/editor/editor.main.css" />
href="https://www.unpkg.com/monaco-editor@0.54.0/min/vs/editor/editor.main.css" />
<link
rel="stylesheet"
type="text/css"
@@ -453,7 +454,7 @@ http:
</div>
</dialog>
<script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script>
<script src="https://www.unpkg.com/monaco-editor@0.54.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 src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>

View File

@@ -141,10 +141,32 @@ async function get_actions() {
* @returns {Promise<{}>}
*/
async function get_schema() {
const res = await fetch(
'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json'
);
return res.json();
const searchParams = new URL(window.location).searchParams;
const url = searchParams.get('schema');
if (url) return (await fetch(url)).json();
const branch = searchParams.get('branch');
if (branch)
return (
await fetch(
`https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/refs/heads/${branch}/config.schema.json`
)
).json();
const version = searchParams.get('version');
if (version)
return (
await fetch(
`https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/refs/tags/${version}/config.schema.json`
)
).json();
return (
await fetch(
`https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json`
)
).json();
}
/**
@@ -172,10 +194,16 @@ async function generate_schema(entities, devices, areas, actions, schema) {
const oneOf = [];
for (const [id, data] of actions) {
const i_properties = {
action: {
title: data.name,
description: data.description,
const: id,
},
service: {
title: data.name,
description: data.description,
const: id,
deprecated: true,
},
data: {
type: 'object',
@@ -371,12 +399,16 @@ async function generate_schema(entities, devices, areas, actions, schema) {
properties: i_properties,
});
}
schema.$defs.tap_action = {
schema.$defs.tap_action_tap = {
type: 'object',
oneOf: oneOf,
properties: {
action: {
type: 'string',
},
service: {
type: 'string',
deprecated: true,
},
confirm: {
$ref: '#/$defs/confirm',
@@ -389,8 +421,16 @@ async function generate_schema(entities, devices, areas, actions, schema) {
properties: {},
},
},
anyOf: [
{
required: ['action'],
},
{
required: ['service'],
},
],
};
delete schema.$defs.tap.properties.service;
delete schema.$defs.tap.properties.action;
delete schema.$schema;
return schema;
@@ -789,14 +829,16 @@ require(['vs/editor/editor.main'], async () => {
const runAction = editor.addCommand(
0,
async function (_, action) {
const service = action.tap_action.service.split('.');
let data = action.tap_action.data;
async function (_, tap) {
const action = (tap.tap_action.action ?? tap.tap_action.service).split(
'.'
);
let data = tap.tap_action.data;
if (data) {
data.entity_id = action.entity;
data.entity_id = tap.entity;
} else {
data = {
entity_id: action.entity,
entity_id: tap.entity,
};
}
const t = toast({
@@ -804,7 +846,7 @@ require(['vs/editor/editor.main'], async () => {
});
try {
const res = await fetch(
api_url + '/services/' + service[0] + '/' + service[1],
api_url + '/services/' + action[0] + '/' + action[1],
{
method: 'POST',
headers: {
@@ -1130,7 +1172,7 @@ require(['vs/editor/editor.main'], async () => {
if (node.type === 'property') {
if (node.key[0].value === 'tap_action') {
const d = get(data, path);
if (d.tap_action.service) {
if (d.tap_action.action ?? d.tap_action.service) {
lenses.push({
range: {
startLineNumber: node.key[0].range.start.line + 1,

View File

@@ -10,10 +10,10 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/toastify-js": "^1.12.4",
"@types/toastify-js": "1.12.0",
"@vscode/webview-ui-toolkit": "1.4.0",
"json-ast-comments": "1.1.1",
"monaco-editor": "0.52.2",
"monaco-editor": "0.54.2",
"prettier": "^3.6.2",
"serve": "^14.2.1"
}

8
web/pnpm-lock.yaml generated
View File

@@ -6,8 +6,8 @@ settings:
devDependencies:
'@types/toastify-js':
specifier: ^1.12.4
version: 1.12.4
specifier: 1.12.0
version: 1.12.0
'@vscode/webview-ui-toolkit':
specifier: 1.4.0
version: 1.4.0(react@18.2.0)
@@ -55,8 +55,8 @@ packages:
exenv-es6: 1.1.1
dev: true
/@types/toastify-js@1.12.4:
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
/@types/toastify-js@1.12.0:
resolution: {integrity: sha512-fqpDHaKhFukN9KRm24bbH0wozvHmSwjvkaLjBUrWcSfSS4zysIwTYqNLG3XbSNhRlsTNRNLGS23tp/VhPwsfHQ==}
dev: true
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):