Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
6db7b67536 | |||
e5df010af8 | |||
ee964ce882 | |||
ee9da24592 | |||
906cdf7371 | |||
7dd85937fa | |||
d141c03104 | |||
842a31a1cc | |||
5639ff5c42 | |||
8b3e86a00f | |||
6dbcea94cf | |||
b28daacafd | |||
029a9f373e | |||
ec044c5408 | |||
659a060c76 | |||
3acef26fea | |||
e898fc1fe5 | |||
4df1fd69bc | |||
fc19599586 | |||
13f70af45a | |||
90ed1f4bea | |||
2117b27210 | |||
df7874e825 | |||
f3c5947b82 | |||
47a930828a | |||
ef299bcaf6 | |||
f0eb9c26b1 | |||
f1c592179d | |||
4ed81df60a | |||
b4f5f34760 | |||
4cf5a0ae26 | |||
b44d4c6155 | |||
f2d65aa6e3 | |||
6b2aa3135a | |||
907848b5fb | |||
5f34f870d9 | |||
ada18f8323 | |||
f74a3168de | |||
7e58e5416d |
@ -1,4 +1,4 @@
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | Battery Reporting | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
|
||||
# Background Service
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | Version History
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
|
||||
# Version History
|
||||
|
||||
@ -34,9 +34,13 @@
|
||||
| 2.19 | A template to evaluate is now optionally allowed on both `group` and `toggle` menu items. The template to evaluate is non-optional on a `template` menu item. All updates are performed in a single HTTP GET request for efficiency. Bug fix for negative heading values. Vibration now (optionally) confirms toggle menu items being tapped. |
|
||||
| 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.22 | <img src="images/pin_view.png" width="200" title="PIN Entry View"/><br/>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. |
|
||||
| 2.30 | <img src="images/Venu2_glance_default.png" width="200" title="Default Glance"/><br/>Extensive re-work of the [Glance](examples/Glance.md) view, including the ability to customise it with a user supplied template. |
|
||||
| 2.31 | Adding [two new options](./examples/Actions.md#exit-on-tap) to the menu items: 1) The ability to disable a menu item, e.g. temporarily for seasonal changes, 2) The option to exit after a menu item has been select. |
|
||||
|
@ -4,5 +4,9 @@
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
"settings": {
|
||||
"cSpell.words": [
|
||||
"Initialiser"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
Home | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
|
||||
# GarminHomeAssistant
|
||||
|
||||
@ -24,7 +24,7 @@ As of version 2.0, there are now two installable versions. For older devices bef
|
||||
|
||||
| Version | Explanation |
|
||||
|------------------------|-------------|
|
||||
| Application (original) | For newer devices that allow glance views in their applications, e.g. Venu 2, the GarminHomeAssistant application can be started either from a glance or from the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d28-ec5e-438d-9f83-39e9f45b199d) application page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the application. The application can be started two different ways, either from the glance in the carousel, or as an application from the list of applications & activities. With the latter, it is worth marking the application as a favourite.<br/><img src="images/Venu2_app_start.png" width="200" title="Venu 2" style="margin:5px"/><img src="images/Vivoactive3_app_start.jpg" width="200" title="Venu 2" style="margin:5px"/><br/>If you place the application on your list of favourites, and rearrange it to appear near the top, then the item is just one button press away from the watch face. This second picture here shows the application menu on a Vivoactive 3 watch.<br/><img src="images/Venu2_glance_start.png" width="200" title="Venu 2" style="margin:5px"/><br/>On newer watches, you can also start the application from the glance carousel. The glance view here typically displays some trackable status, so ours provides some early indication of availability. Older watches will still allow you to start this application from the list of applications and activities. |
|
||||
| Application (original) | For newer devices that allow glance views in their applications, e.g. Venu 2, the GarminHomeAssistant application can be started either from a glance or from the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d28-ec5e-438d-9f83-39e9f45b199d) application page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the application. The application can be started two different ways, either from the glance in the carousel, or as an application from the list of applications & activities. With the latter, it is worth marking the application as a favourite.<br/><img src="images/Venu2_app_start.png" width="200" title="Venu 2" style="margin:5px"/><img src="images/Vivoactive3_app_start.jpg" width="200" title="Vivoactive 3" style="margin:5px"/><br/>If you place the application on your list of favourites, and rearrange it to appear near the top, then the item is just one button press away from the watch face. This second picture here shows the application menu on a Vivoactive 3 watch.<br/><img src="images/Venu2_glance_default.png" width="200" title="Venu 2" style="margin:5px"/><br/>On newer watches, you can also start the application from the glance carousel. The glance view here typically displays some trackable status, so ours provides some early indication of availability. Older watches will still allow you to start this application from the list of applications and activities. |
|
||||
| Widget | **"Maintenance only mode"** so no new features will be added to this version.<br>For older devices that use widgets, e.g. Venu (1) as opposed to applications with "glances", the GarminHomeAssistant application can instead be started from the widget carousel. This is a separate item in the Connect IQ AppStore and with this installation, the application will no longer appear in the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/) widget page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the widget.<br/><img src="images/Venu_Widget_sim.png" width="200" title="Venu 2" style="margin:5px"/><br/>Typically the widget view implements something similar to the glance view, e.g. status, and exists in a widget carousel to allow you to select an application to launch.<br>**Please note that memory in widgets is more limited than applications. This means a large menu definition can crash the widget without the code catching the error.**<br> This version was born out of the application version and from Ver 2.0 shared the same source code repository until Ver 2.8 when they were [separated](https://github.com/house-of-abbey/GarminHomeAssistantWidget) to allow the application version to take advantage of its increase memory availability. |
|
||||
|
||||
### Features
|
||||
|
@ -1,4 +1,4 @@
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Background Service](BackgroundService.md) | Trouble Shooting | [Version History](HISTORY.md)
|
||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||
|
||||
# Troubleshooting Guides
|
||||
|
||||
|
@ -33,44 +33,49 @@ 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%
|
||||
|
||||
|
@ -2,14 +2,19 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "The schema will prevent JSON file errors."
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Top level menu title"
|
||||
},
|
||||
"glance": {
|
||||
"$ref": "#/$defs/glance"
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/$defs/items"
|
||||
},
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "items"],
|
||||
@ -29,8 +34,7 @@
|
||||
"const": "toggle"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a toggle."
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"tap_action": {
|
||||
"type": "object",
|
||||
@ -43,6 +47,12 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
},
|
||||
"exit": {
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["entity", "name", "type"],
|
||||
@ -71,6 +81,9 @@
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'info' or 'tap' instead."
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
@ -97,6 +110,12 @@
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
},
|
||||
"exit": {
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type", "tap_action"],
|
||||
@ -116,6 +135,9 @@
|
||||
"type": {
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "info"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
@ -135,8 +157,7 @@
|
||||
"const": "tap"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a tap."
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"service": {
|
||||
"$ref": "#/$defs/entity",
|
||||
@ -146,6 +167,12 @@
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
},
|
||||
"exit": {
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"],
|
||||
@ -174,11 +201,13 @@
|
||||
"const": "group"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a group."
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/$defs/items"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "title", "type", "items"],
|
||||
@ -190,7 +219,6 @@
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"maxItems": 16,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
@ -248,7 +276,8 @@
|
||||
"required": ["service"]
|
||||
},
|
||||
"content": {
|
||||
"title": "Jinja2 template defining the text to display.",
|
||||
"title": "Home Assistant Template",
|
||||
"description": "Jinja2 template defining the text to display. Must be included in an 'info'. Optional in a 'toggle', 'tap' and 'group'. Special characters may not render in the glance context.",
|
||||
"type": "string"
|
||||
},
|
||||
"confirm": {
|
||||
@ -262,6 +291,47 @@
|
||||
"default": false,
|
||||
"title": "PIN Confirmation",
|
||||
"description": "Optional PIN confirmation of the action before execution as a precaution. Has precedence over 'confirm': true if both are set."
|
||||
},
|
||||
"glance": {
|
||||
"type": "object",
|
||||
"title": "Glance customisation",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"title": "Glance type",
|
||||
"description": "One of 'info' or 'status'. 'info' renders the template specified in the 'content' field inside the glance view. 'status' reverts to the default glance view and ignores the 'content' field. This allows for disabling the template temporarily.",
|
||||
"const": "info"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content"
|
||||
}
|
||||
},
|
||||
"required": ["type", "content"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"title": "Glance type",
|
||||
"description": "One of 'info' or 'status'.",
|
||||
"const": "status"
|
||||
}
|
||||
},
|
||||
"required": ["type"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"title": "Enable the menu item",
|
||||
"description": "Typically used to temporarily disable a menu item, e.g. for seasonal variations. Enabled (true) by default."
|
||||
},
|
||||
"exit": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Exit on selection",
|
||||
"description": "Choose to exit the application after this item has been selected. Disabled (false) by default. N.B. Only actionable menu items can have this field added."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
[Home](../README.md) | [Switches](Switches.md) | Actions | [Templates](Templates.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
|
||||
|
||||
# Actions
|
||||
|
||||
@ -75,3 +76,35 @@ Note that for notify events, you _must_ not supply an `entity_id` or the API cal
|
||||
> 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.
|
||||
|
||||
## Exit on Tap
|
||||
|
||||
You can choose individual items that will quit after they have completed their action.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "automation.turn_off_stuff",
|
||||
"name": "Turn off Stuff",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "automation.trigger"
|
||||
},
|
||||
"exit": true
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Menu Item
|
||||
|
||||
If you would like to temporarily disable an item in your menu, e.g. for seasonal reasons like not needing to turn on the heating in summer, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "automation.turn_off_stuff",
|
||||
"name": "Turn off Stuff",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "automation.trigger"
|
||||
},
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
80
examples/Glance.md
Normal file
@ -0,0 +1,80 @@
|
||||
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
|
||||
# Glance
|
||||
|
||||
Since [version 2.30](../History.md), it is possible to ovverride the text displayed on the Glance view. This page explains how to customise the text.
|
||||
|
||||
|
||||
## Default View
|
||||
|
||||
The default view has always been to display the status of the menu and API availability to indicate if there's a problem. This view has now been updated to be more colourful.
|
||||
|
||||
<img src="../images/Venu2_glance_default.png" width="200" title="Venu 2 Default Glance"/>
|
||||
|
||||
When either the API or the menu file is inaccessible, the fields will turn red.
|
||||
|
||||
|
||||
## Customised View
|
||||
|
||||
In order to customise the Glance view you need to add a `glance` field to the top level of the JSON menu file as illustrated here:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json",
|
||||
"title": "Home",
|
||||
"glance": {
|
||||
"type": "info",
|
||||
"content": "Text: {% .. %}"
|
||||
},
|
||||
"items": [...]
|
||||
}
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
<img src="../images/Venu2_glance_custom.png" width="200" title="Venu 2 Customised Glance"/>
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json",
|
||||
"glance": {
|
||||
"type": "info",
|
||||
"content": "Solar Battery: {{ states('sensor.battery_capacity_charge') }}%"
|
||||
},
|
||||
:
|
||||
}
|
||||
```
|
||||
|
||||
You may make this as complicated as you like! But you have limited space and only ASCII text characters. **It is best to turn on menu caching in order to speed up the display of the template**. The display is then nearly instantaneous.
|
||||
|
||||
The default view will persist showing until the errors are resolved. In order to extract the custom glance template both the menu and the API are required. So it is logical that the two tests must pass first. The exception here is if the menu is cached, in which case only the API needs to pass.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Sadly what you cannot do is use special characters like: 🌞🔋⛅🪫. Whilst these do display in menu items, they do not seem to work on the Glance view. We really like them, so have tried but failed. Only ASCII text appears to be supported by the Garmin Connect IQ SDK's Glance View. This is not something we have any control over, please do not request this to be "fixed".
|
||||
|
||||
It is possible to revert to the default glance content without deleting the template by changing the `type` to `status`.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json",
|
||||
"title": "Home",
|
||||
"glance": {
|
||||
"type": "status",
|
||||
"content": "Text: {% .. %}"
|
||||
},
|
||||
"items": [...]
|
||||
}
|
||||
```
|
||||
|
||||
So the glance view object has a `type` field with two possible values: `info` and `status`. When the type is `status` the `content` field is not required.
|
||||
|
||||
|
||||
## Displayed Errors
|
||||
|
||||
The following shows the default glance when the menu file is not available at the specified URL.
|
||||
|
||||
<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.
|
||||
|
||||
<img src="../images/Venu2_glance_no_bt.png" width="200" title="Venu 2 Glance showing lost connectivity"/>
|
@ -1,4 +1,5 @@
|
||||
[Home](../README.md) | Switches | [Actions](Actions.md) | [Templates](Templates.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
|
||||
|
||||
# Switches
|
||||
|
||||
@ -108,3 +109,29 @@ Then you can use the following in your config:
|
||||
"type": "toggle"
|
||||
}
|
||||
```
|
||||
|
||||
## Exit On Toggle
|
||||
|
||||
You can choose individual items that will quit after they have completed their action.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "light.hall_light",
|
||||
"name": "Hall Light & Quit",
|
||||
"type": "toggle",
|
||||
"exit": true
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Menu Item
|
||||
|
||||
If you would like to temporarily disable an item in your menu, e.g. for seasonal reasons like not needing to turn on Christmas tree lights outside the festive season, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "light.chrissmas_tree",
|
||||
"name": "Christmas Lights",
|
||||
"type": "toggle",
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | Templates | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||
|
||||
# Templates
|
||||
|
||||
@ -218,6 +218,19 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Menu Item
|
||||
|
||||
If you would like to temporarily disable an item in your menu, then rather than swapping menu definition files or deleting a section of the menu you can mark the item as 'disabled'. This field applies to all menu items.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Phone",
|
||||
"type": "info",
|
||||
"content": "{{ ... }}",
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
## Warnings
|
||||
|
||||
Just remember, on older smaller memory devices **you have the ability to crash the application by creating an excessive menu definition**. Templates can require significant definition for highly customised text. Don't be silly.
|
||||
|
82
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,13 +100,11 @@ 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 ^
|
||||
--package-app ^
|
||||
--release
|
||||
rem --warn
|
||||
|
||||
echo.
|
||||
echo Finished exporting HomeAssistant
|
||||
|
BIN
images/Venu2_Glance_good.png
Normal file
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
BIN
images/Venu2_glance_custom.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
images/Venu2_glance_default.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
images/Venu2_glance_no_bt.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
images/Venu2_glance_no_menu.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 9.1 KiB |
@ -24,10 +24,6 @@
|
||||
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 command palette to edit
|
||||
@ -108,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"/>
|
||||
@ -116,6 +114,7 @@
|
||||
<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"/>
|
||||
@ -159,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
|
||||
|
@ -87,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
|
||||
@ -115,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
|
||||
@ -124,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
|
||||
@ -141,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
|
||||
@ -157,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
|
||||
@ -169,9 +169,9 @@ 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
|
||||
# Screen Size 240x400 launcher icon size 38x33
|
||||
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-w
|
||||
@ -229,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
|
||||
@ -241,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
|
||||
|
@ -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();
|
||||
|
@ -11,30 +11,38 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant centralised constants.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
static const scApiBackoff = 1000; // ms
|
||||
// Needs to be long enough to enable a "double ESC" to quit the application from
|
||||
// an ErrorView.
|
||||
|
||||
//! Time to let the existing HTTP responses get serviced after a
|
||||
//! `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` response code.
|
||||
static const scApiBackoff = 2000; // ms
|
||||
|
||||
//! 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;
|
||||
}
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Application root for GarminHomeAssistant
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
@ -25,21 +20,27 @@ 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;
|
||||
private var mMenuStatus as Lang.String or Null;
|
||||
private var mHaMenu as HomeAssistantView or Null;
|
||||
private var mQuitTimer as QuitTimer or Null;
|
||||
private var mGlanceTimer as Timer.Timer or Null;
|
||||
private var mUpdateTimer as Timer.Timer or Null;
|
||||
private var mApiStatus as Lang.String or Null;
|
||||
private var mMenuStatus as Lang.String or Null;
|
||||
private var mHaMenu as HomeAssistantView or Null;
|
||||
private var mGlanceTemplate as Lang.String or Null = null;
|
||||
private var mGlanceText as Lang.String or Null = null;
|
||||
private var mQuitTimer as QuitTimer or Null;
|
||||
private var mGlanceTimer as Timer.Timer or Null;
|
||||
private var mUpdateTimer as Timer.Timer or Null;
|
||||
// Array initialised by onReturnFetchMenuConfig()
|
||||
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> or Null;
|
||||
private var mIsGlance as Lang.Boolean = false;
|
||||
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 mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> or Null;
|
||||
private var mIsGlance as Lang.Boolean = false;
|
||||
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 = {};
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
// ATTENTION when adding stuff into this block:
|
||||
@ -55,7 +56,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:
|
||||
@ -71,7 +75,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:
|
||||
@ -87,7 +95,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();
|
||||
@ -132,10 +143,16 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@ -188,7 +205,9 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
}
|
||||
}
|
||||
if (!mIsGlance) {
|
||||
if (mIsGlance) {
|
||||
glanceTemplate(data);
|
||||
} else {
|
||||
if (data == null) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
} else {
|
||||
@ -208,8 +227,11 @@ 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("Menu URL = " + Settings.getConfigUrl());
|
||||
@ -254,7 +276,9 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
} else {
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Cached) as Lang.String;
|
||||
WatchUi.requestUpdate();
|
||||
if (!mIsGlance) {
|
||||
if (mIsGlance) {
|
||||
glanceTemplate(menu);
|
||||
} else {
|
||||
buildMenu(menu);
|
||||
}
|
||||
return true;
|
||||
@ -263,6 +287,10 @@ 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();
|
||||
@ -271,6 +299,8 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
} // 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.
|
||||
@ -279,7 +309,31 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
|
||||
//! Extract the optional template to override the default glance view.
|
||||
//
|
||||
function glanceTemplate(menu as Lang.Dictionary) {
|
||||
if (menu != null) {
|
||||
if (menu.get("glance") != null) {
|
||||
var glance = menu.get("glance") as Lang.Dictionary;
|
||||
if (glance.get("type").equals("info")) {
|
||||
mGlanceTemplate = glance.get("content") as Lang.String;
|
||||
// System.println("HomeAssistantApp glanceTemplate() " + mGlanceTemplate);
|
||||
} else { // if glance.get("type").equals("status")
|
||||
mGlanceTemplate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! 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);
|
||||
|
||||
@ -353,6 +407,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.");
|
||||
@ -368,24 +424,23 @@ 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
|
||||
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
|
||||
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
|
||||
Communications.makeWebRequest(
|
||||
url,
|
||||
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
|
||||
{
|
||||
"type" => "render_template",
|
||||
"data" => mTemplates
|
||||
@ -402,10 +457,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);
|
||||
|
||||
@ -466,6 +527,8 @@ 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());
|
||||
@ -506,30 +569,153 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//! Callback function after completing the GET request to render the glance template.
|
||||
//!
|
||||
//! @param responseCode Response code.
|
||||
//! @param data Response data.
|
||||
//
|
||||
(:glance)
|
||||
function onReturnFetchGlanceContent(
|
||||
responseCode as Lang.Number,
|
||||
data as Null or Lang.Dictionary or Lang.String
|
||||
) as Void {
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Data: " + data);
|
||||
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
case Communications.BLE_QUEUE_FULL:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent() Response Code: 404, page not found. Check Configuration URL setting.");
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ConfigUrlNotFound) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
case 200:
|
||||
if (data != null) {
|
||||
mGlanceText = data.get("glanceTemplate");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// System.println("HomeAssistantApp onReturnFetchGlanceContent(): Unhandled HTTP response code = " + responseCode);
|
||||
if (!mIsGlance) {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
}
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Construct the GET request to convert the optional glance template to text for display.
|
||||
//
|
||||
(:glance)
|
||||
function fetchGlanceContent() as Void {
|
||||
if (mGlanceTemplate != null) {
|
||||
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
|
||||
Communications.makeWebRequest(
|
||||
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
|
||||
{
|
||||
"type" => "render_template",
|
||||
"data" => {
|
||||
"glanceTemplate" => {
|
||||
"template" => mGlanceTemplate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||
:headers => {
|
||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnFetchGlanceContent)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//! 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 optional glance text that overrides the default glance content. This
|
||||
//! is derived from the glance template.
|
||||
//!
|
||||
//! @return A string derived from the glance template
|
||||
//
|
||||
(:glance)
|
||||
function getGlanceText() as Lang.String or Null {
|
||||
return mGlanceText;
|
||||
}
|
||||
|
||||
//! 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) {
|
||||
@ -539,10 +725,18 @@ 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;
|
||||
@ -554,30 +748,47 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
return [new HomeAssistantGlanceView(self)];
|
||||
}
|
||||
|
||||
// Required for the Glance update timer.
|
||||
//! Return the glance theme.
|
||||
//!
|
||||
//! @return The glance colour
|
||||
//
|
||||
function getGlanceTheme() as Application.AppBase.GlanceTheme {
|
||||
return Application.AppBase.GLANCE_THEME_LIGHT_BLUE;
|
||||
}
|
||||
|
||||
//! Update the menu and API statuses. Required for the Glance update timer.
|
||||
//
|
||||
function updateStatus() as Void {
|
||||
mGlanceTimer = null;
|
||||
fetchMenuConfig();
|
||||
fetchApiStatus();
|
||||
fetchGlanceContent();
|
||||
}
|
||||
|
||||
//! 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,29 +11,49 @@
|
||||
//
|
||||
// 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
|
||||
private static const scMidSep = 10; // Middle Separator "text:_text" in pixels
|
||||
private var mApp as HomeAssistantApp;
|
||||
private var mTitle as WatchUi.Text or Null;
|
||||
private var mApiText as WatchUi.Text or Null;
|
||||
private var mApiStatus as WatchUi.Text or Null;
|
||||
private var mMenuText as WatchUi.Text or Null;
|
||||
private var mMenuStatus as WatchUi.Text or Null;
|
||||
private var mAntiAlias as Lang.Boolean = false;
|
||||
//! Margin left of the filled rectangle in pixels.
|
||||
private static const scLeftRectMargin = 5;
|
||||
//! Filled rectangle width in pixels.
|
||||
private static const scRectWidth = 20;
|
||||
//! Margin right of the filled rectangle in pixels.
|
||||
private static const scRightRectMargin = 5;
|
||||
//! Separator between the first column of text and the second in pixels.
|
||||
//! i.e. Middle Separator "text:_text"
|
||||
private static const scMidSep = 10;
|
||||
//! Margin on the right side of the glance in pixels.
|
||||
private static const scRightGlanceMargin = 15;
|
||||
//! Internal margin for the custom template between the border and the text in pixels.
|
||||
private static const scIntCustMargin = 5;
|
||||
//! Margin top and bottom of the rectangles in pixels.
|
||||
private static const scVertMargin = 5;
|
||||
//! Size of the rounded rectangle corners in pixels.
|
||||
private static const scRectRadius = 5;
|
||||
|
||||
//! Dynamically scale the width of the first column of text based on the
|
||||
//! language selection for the word "Menu".
|
||||
private var mTextWidth as Lang.Number = 0;
|
||||
// Re-usable text items for drawing
|
||||
private var mApp as HomeAssistantApp;
|
||||
private var mTitle as WatchUi.Text or Null;
|
||||
private var mApiText as WatchUi.Text or Null;
|
||||
private var mApiStatus as WatchUi.Text or Null;
|
||||
private var mMenuText as WatchUi.Text or Null;
|
||||
private var mMenuStatus as WatchUi.Text or Null;
|
||||
private var mGlanceContent as WatchUi.TextArea or Null;
|
||||
private var mAntiAlias as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//
|
||||
function initialize(app as HomeAssistantApp) {
|
||||
GlanceView.initialize();
|
||||
mApp = app;
|
||||
@ -42,16 +62,21 @@ 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);
|
||||
var h = dc.getHeight();
|
||||
|
||||
mTextWidth = dc.getTextWidthInPixels(WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String + ":", Graphics.FONT_XTINY);
|
||||
|
||||
mTitle = new WatchUi.Text({
|
||||
:text => WatchUi.loadResource($.Rez.Strings.AppName) as Lang.String,
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_TINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftMargin,
|
||||
:locX => scLeftRectMargin,
|
||||
:locY => 1 * h / 6
|
||||
});
|
||||
|
||||
@ -60,7 +85,7 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_XTINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftMargin,
|
||||
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin,
|
||||
:locY => 3 * h / 6
|
||||
});
|
||||
mApiStatus = new WatchUi.Text({
|
||||
@ -68,15 +93,16 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_XTINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftMargin + scMidSep + tw,
|
||||
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scMidSep + mTextWidth,
|
||||
:locY => 3 * h / 6
|
||||
});
|
||||
|
||||
mMenuText = new WatchUi.Text({
|
||||
:text => WatchUi.loadResource($.Rez.Strings.GlanceMenu) as Lang.String + ":",
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_XTINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftMargin,
|
||||
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin,
|
||||
:locY => 5 * h / 6
|
||||
});
|
||||
mMenuStatus = new WatchUi.Text({
|
||||
@ -84,14 +110,38 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_XTINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftMargin + scMidSep + tw,
|
||||
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scMidSep + mTextWidth,
|
||||
:locY => 5 * h / 6
|
||||
});
|
||||
|
||||
mGlanceContent = new WatchUi.TextArea({
|
||||
:text => "A longer piece of text to wrap.",
|
||||
:color => Graphics.COLOR_WHITE,
|
||||
:font => Graphics.FONT_XTINY,
|
||||
:justification => Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER,
|
||||
:locX => scLeftRectMargin + scRectWidth + scRightRectMargin + scIntCustMargin,
|
||||
:locY => (2 * h / 6) + scVertMargin,
|
||||
:width => dc.getWidth() - scLeftRectMargin - scRectWidth - scRightRectMargin - (2 * scIntCustMargin) - scRightGlanceMargin,
|
||||
:height => (4 * h / 6) - (2 * scVertMargin)
|
||||
});
|
||||
}
|
||||
|
||||
//! Update the view with the latest status text.
|
||||
//!
|
||||
//! @param dc Device context
|
||||
//
|
||||
function onUpdate(dc as Graphics.Dc) as Void {
|
||||
var h = dc.getHeight();
|
||||
var w = dc.getWidth() - scLeftRectMargin - scRightGlanceMargin;
|
||||
var apiStatus = mApp.getApiStatus();
|
||||
var menuStatus = mApp.getMenuStatus();
|
||||
var glanceText = mApp.getGlanceText();
|
||||
var apiCol;
|
||||
var menuCol;
|
||||
// System.println("HomeAssistantGlanceView onUpdate() glanceText=" + glanceText);
|
||||
|
||||
GlanceView.onUpdate(dc);
|
||||
if(mAntiAlias) {
|
||||
if (mAntiAlias) {
|
||||
dc.setAntiAlias(true);
|
||||
}
|
||||
dc.setColor(
|
||||
@ -99,12 +149,60 @@ class HomeAssistantGlanceView extends WatchUi.GlanceView {
|
||||
Graphics.COLOR_TRANSPARENT
|
||||
);
|
||||
dc.clear();
|
||||
mTitle.setColor(Graphics.COLOR_BLUE);
|
||||
mTitle.draw(dc);
|
||||
mApiText.draw(dc);
|
||||
mApiStatus.setText(mApp.getApiStatus());
|
||||
mApiStatus.draw(dc);
|
||||
mMenuText.draw(dc);
|
||||
mMenuStatus.setText(mApp.getMenuStatus());
|
||||
mMenuStatus.draw(dc);
|
||||
}
|
||||
|
||||
if (apiStatus.equals(WatchUi.loadResource($.Rez.Strings.Checking))) {
|
||||
apiCol = Graphics.COLOR_YELLOW;
|
||||
} else if (apiStatus.equals(WatchUi.loadResource($.Rez.Strings.Available))) {
|
||||
apiCol = Graphics.COLOR_GREEN;
|
||||
} else {
|
||||
apiCol = Graphics.COLOR_RED;
|
||||
}
|
||||
|
||||
if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Checking))) {
|
||||
menuCol = Graphics.COLOR_YELLOW;
|
||||
} else if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Available))) {
|
||||
menuCol = Graphics.COLOR_GREEN;
|
||||
} else if (menuStatus.equals(WatchUi.loadResource($.Rez.Strings.Cached))) {
|
||||
menuCol = Graphics.COLOR_GREEN;
|
||||
} else {
|
||||
menuCol = Graphics.COLOR_RED;
|
||||
}
|
||||
|
||||
if (glanceText == null) {
|
||||
// Default Glance View
|
||||
mApiText.draw(dc);
|
||||
mApiStatus.setText(apiStatus);
|
||||
mApiStatus.setColor(apiCol);
|
||||
dc.setColor(apiCol, apiCol);
|
||||
dc.drawRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, w, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
mApiStatus.draw(dc);
|
||||
|
||||
mMenuText.draw(dc);
|
||||
mMenuStatus.setText(menuStatus);
|
||||
mMenuStatus.setColor(menuCol);
|
||||
dc.setColor(menuCol, menuCol);
|
||||
dc.drawRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, w, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
dc.fillRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
mMenuStatus.draw(dc);
|
||||
} else {
|
||||
// Customised Glance View
|
||||
dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_BLUE);
|
||||
dc.drawRoundedRectangle(
|
||||
scLeftRectMargin + scRectWidth + scRightRectMargin,
|
||||
2 * h / 6 + scVertMargin,
|
||||
w - scRectWidth - scRightRectMargin,
|
||||
4 * h / 6 - (2 * scVertMargin),
|
||||
scRectRadius
|
||||
);
|
||||
dc.setColor(apiCol, apiCol);
|
||||
dc.fillRoundedRectangle(scLeftRectMargin, 2 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
dc.setColor(menuCol, menuCol);
|
||||
dc.fillRoundedRectangle(scLeftRectMargin, 4 * h / 6 + scVertMargin, scRectWidth, 2 * h / 6 - (2 * scVertMargin), scRectRadius);
|
||||
mGlanceContent.setText(glanceText);
|
||||
mGlanceContent.draw(dc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,20 +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;
|
||||
|
||||
//! 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,
|
||||
@ -48,6 +47,8 @@ class HomeAssistantGroupMenuItem extends HomeAssistantMenuItem {
|
||||
mMenu = new HomeAssistantView(definition, null);
|
||||
}
|
||||
|
||||
//! Return the submenu for this group menu item.
|
||||
//
|
||||
function getMenuView() as HomeAssistantView {
|
||||
return mMenu;
|
||||
}
|
||||
|
@ -11,20 +11,23 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Generic menu button with an icon that optionally renders a Home Assistant Template.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
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,
|
||||
@ -43,15 +46,28 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
|
||||
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;
|
||||
}
|
||||
|
||||
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 updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
//! 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) {
|
||||
@ -76,4 +92,4 @@ class HomeAssistantMenuItem extends WatchUi.IconMenuItem {
|
||||
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,66 +65,95 @@ 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 options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||
//
|
||||
function toggle(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity_id as Lang.String or Null,
|
||||
template as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
pin as Lang.Boolean
|
||||
options as {
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean
|
||||
}
|
||||
) as WatchUi.MenuItem {
|
||||
var keys = mMenuItemOptions.keys();
|
||||
for (var i = 0; i < keys.size(); i++) {
|
||||
options.put(keys[i], mMenuItemOptions.get(keys[i]));
|
||||
}
|
||||
return new HomeAssistantToggleMenuItem(
|
||||
label,
|
||||
template,
|
||||
confirm,
|
||||
pin,
|
||||
{ "entity_id" => entity_id },
|
||||
mMenuItemOptions
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
//! 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 data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
|
||||
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||
//
|
||||
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,
|
||||
data as Lang.Dictionary or Null,
|
||||
options as {
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean
|
||||
}
|
||||
) 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);
|
||||
}
|
||||
}
|
||||
var keys = mMenuItemOptions.keys();
|
||||
for (var i = 0; i < keys.size(); i++) {
|
||||
options.put(keys[i], mMenuItemOptions.get(keys[i]));
|
||||
}
|
||||
if (service != null) {
|
||||
options.put(:icon, mTapTypeIcon);
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
template,
|
||||
service,
|
||||
confirm,
|
||||
pin,
|
||||
data,
|
||||
mTapTypeIcon,
|
||||
mMenuItemOptions,
|
||||
options,
|
||||
mHomeAssistantService
|
||||
);
|
||||
} else {
|
||||
options.put(:icon, mInfoTypeIcon);
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
template,
|
||||
service,
|
||||
confirm,
|
||||
pin,
|
||||
null,
|
||||
data,
|
||||
mInfoTypeIcon,
|
||||
mMenuItemOptions,
|
||||
options,
|
||||
mHomeAssistantService
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//! 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
|
||||
|
@ -11,25 +11,28 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Pin Confirmation dialog and logic.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
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,14 +35,24 @@ 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,
|
||||
data as Null or Lang.Dictionary or Lang.String,
|
||||
context as Lang.Object
|
||||
) as Void {
|
||||
var entity_id = context as Lang.String or Null;
|
||||
var c = context as Lang.Dictionary;
|
||||
var entity_id;
|
||||
var exit = false;
|
||||
if (c != null) {
|
||||
entity_id = c.get(:entity_id) as Lang.String;
|
||||
exit = c.get(:exit) as Lang.Boolean;
|
||||
}
|
||||
// System.println("HomeAssistantService onReturnCall() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantService onReturnCall() Response Data: " + data);
|
||||
|
||||
@ -99,6 +108,9 @@ class HomeAssistantService {
|
||||
:bgcolor => Graphics.COLOR_BLACK
|
||||
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
if (exit) {
|
||||
System.exit();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -107,9 +119,15 @@ 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
|
||||
data as Lang.Dictionary or Null,
|
||||
exit as Lang.Boolean
|
||||
) as Void {
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
||||
@ -141,7 +159,10 @@ class HomeAssistantService {
|
||||
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
|
||||
:context => entity_id
|
||||
:context => {
|
||||
:entity_id => entity_id,
|
||||
:exit => exit
|
||||
}
|
||||
},
|
||||
method(:onReturnCall)
|
||||
);
|
||||
|
@ -11,57 +11,69 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Menu button that triggers a service.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
//! Menu button that triggers a service.
|
||||
//
|
||||
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
private var mHomeAssistantService as HomeAssistantService;
|
||||
private var mService as Lang.String or Null;
|
||||
private var mService as Lang.String or Null;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mExit 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 data Data to supply to the service call.
|
||||
//! @param exit Should the service call complete and then exit?
|
||||
//! @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 icon Icon to use for the menu item.
|
||||
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||
//! @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,
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
pin as Lang.Boolean,
|
||||
service as Lang.String or Null,
|
||||
data as Lang.Dictionary or Null,
|
||||
icon as Graphics.BitmapType or WatchUi.Drawable,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean
|
||||
} or Null,
|
||||
haService as HomeAssistantService
|
||||
) {
|
||||
if (options != null) {
|
||||
options.put(:icon, icon);
|
||||
} else {
|
||||
options = { :icon => icon };
|
||||
}
|
||||
|
||||
HomeAssistantMenuItem.initialize(
|
||||
label,
|
||||
template,
|
||||
options
|
||||
{
|
||||
:alignment => options.get(:alignment),
|
||||
:icon => options.get(:icon)
|
||||
}
|
||||
);
|
||||
|
||||
mHomeAssistantService = haService;
|
||||
mService = service;
|
||||
mConfirm = confirm;
|
||||
mPin = pin;
|
||||
mData = data;
|
||||
mExit = options.get(:exit);
|
||||
mConfirm = options.get(:confirm);
|
||||
mPin = options.get(:acospin);
|
||||
}
|
||||
|
||||
//! 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) {
|
||||
@ -85,10 +97,13 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
mHomeAssistantService.call(mService, mData, mExit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Light or switch toggle button that calls the API to maintain the up to date state.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -24,22 +19,33 @@ 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;
|
||||
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 mPin as Lang.Boolean;
|
||||
private var mHasVibrate as Lang.Boolean = false;
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param template Menu item template.
|
||||
//! @param data Data to supply to the service call.
|
||||
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||
//
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
confirm as Lang.Boolean,
|
||||
pin as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean
|
||||
} or Null
|
||||
) {
|
||||
WatchUi.ToggleMenuItem.initialize(
|
||||
@ -47,17 +53,23 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
options
|
||||
{
|
||||
:alignment => options.get(:alignment),
|
||||
:icon => options.get(:icon)
|
||||
}
|
||||
);
|
||||
if (Attention has :vibrate) {
|
||||
mHasVibrate = true;
|
||||
}
|
||||
mConfirm = confirm;
|
||||
mPin = pin;
|
||||
mData = data;
|
||||
mTemplate = template;
|
||||
mExit = options.get(:exit);
|
||||
mConfirm = options.get(:confirm);
|
||||
mPin = options.get(:pin);
|
||||
}
|
||||
|
||||
//! 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()) {
|
||||
@ -68,14 +80,27 @@ 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) {
|
||||
@ -100,6 +125,10 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
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");
|
||||
@ -126,9 +155,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);
|
||||
|
||||
@ -181,8 +216,15 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
}
|
||||
getApp().setApiStatus(status);
|
||||
if (mExit) {
|
||||
System.exit();
|
||||
}
|
||||
}
|
||||
|
||||
//! 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());
|
||||
@ -228,6 +270,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) {
|
||||
@ -251,6 +295,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);
|
||||
}
|
||||
|
@ -11,11 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Home Assistant menu construction.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
@ -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 {
|
||||
@ -52,21 +51,95 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
var confirm = false as Lang.Boolean or Null;
|
||||
var pin = false as Lang.Boolean or Null;
|
||||
var data = null as Lang.Dictionary or Null;
|
||||
var enabled = true as Lang.Boolean or Null;
|
||||
var exit = false as Lang.Boolean or Null;
|
||||
if (items[i].get("enabled") != null) {
|
||||
enabled = items[i].get("enabled"); // Optional
|
||||
}
|
||||
if (items[i].get("exit") != null) {
|
||||
exit = items[i].get("exit"); // Optional
|
||||
}
|
||||
if (tap_action != null) {
|
||||
service = tap_action.get("service");
|
||||
confirm = tap_action.get("confirm"); // Optional
|
||||
pin = tap_action.get("pin"); // Optional
|
||||
data = tap_action.get("data"); // Optional
|
||||
if (confirm == null) {
|
||||
confirm = false;
|
||||
data = tap_action.get("data"); // Optional
|
||||
if (tap_action.get("confirm") != null) {
|
||||
confirm = tap_action.get("confirm"); // Optional
|
||||
}
|
||||
if (tap_action.get("pin") != null) {
|
||||
pin = tap_action.get("pin"); // Optional
|
||||
}
|
||||
}
|
||||
if (type != null && name != null) {
|
||||
if (type != null && name != null && enabled) {
|
||||
if (type.equals("toggle") && entity != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm, pin));
|
||||
} else if ((type.equals("tap") && service != null) || (type.equals("info") && content != null) || (type.equals("template") && content != null)) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().toggle(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
{
|
||||
:exit => exit,
|
||||
:confirm => confirm,
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else if (type.equals("tap") && service != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
data,
|
||||
{
|
||||
:exit => exit,
|
||||
:confirm => confirm,
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else if (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));
|
||||
// The exit option is dependent on the type of template.
|
||||
if (tap_action == null) {
|
||||
// No exit from an information only item
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
data,
|
||||
{
|
||||
:exit => false,
|
||||
:confirm => confirm,
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else {
|
||||
// You may exit from template item with a 'tap_action'.
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
data,
|
||||
{
|
||||
: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(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
data,
|
||||
{
|
||||
:exit => false,
|
||||
:confirm => confirm,
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else if (type.equals("group")) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
|
||||
}
|
||||
@ -75,7 +148,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>;
|
||||
@ -102,26 +180,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();
|
||||
|
||||
@ -135,16 +222,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) {
|
||||
@ -164,7 +257,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;
|
||||
}
|
||||
|
@ -11,16 +11,6 @@
|
||||
//
|
||||
// P A Abbey & J D Abbey, SomeoneOnEarth & moesterheld, 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.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
@ -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");
|
||||
@ -71,6 +71,8 @@ class Settings {
|
||||
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;
|
||||
@ -116,65 +118,123 @@ class Settings {
|
||||
// }
|
||||
}
|
||||
|
||||
//! 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) {
|
||||
@ -183,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,7 +123,18 @@ 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:
|
||||
@ -194,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);
|
||||
@ -217,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;
|
||||
|
||||
|
@ -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;
|
||||
|