mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-06-17 20:08:33 +00:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
060cd2729f | |||
73a38e9b4e | |||
b22f119eb3 | |||
cd8f5d900e | |||
ecb2e8bc96 | |||
881700d0ed | |||
75ea23dfbd | |||
2e7216b6b2 | |||
57a44d8946 | |||
4432c7b9a0 | |||
15e2b19193 | |||
1b40231360 | |||
446c579660 | |||
da2e94caa3 | |||
1c182dd615 | |||
fb5c2193f1 | |||
df3be94bf9 | |||
17162c14f2 | |||
b5140ce8b4 | |||
17a5d89998 | |||
63bc127aec | |||
af43fde963 | |||
bbd9119a07 | |||
47a8a6e4e6 | |||
b476da6667 | |||
bd37d5f2a8 | |||
2a48790f9c | |||
0feecde178 | |||
e9a0c5d137 | |||
d387152593 | |||
2ea349bfda | |||
a35798f9d3 | |||
4707f1ea9e | |||
3424576027 | |||
1846d682f7 | |||
685cda7924 | |||
72e825566c | |||
1dc95eeac7 | |||
ea32d71a2b |
@ -31,3 +31,6 @@
|
||||
| 2.16 | Bug fix for lack of phone connection when starting the application. Includes new activity reporting features from [KPWhiver](https://github.com/KPWhiver) covering steps, heart rate, floors climbed and descended, and respiration rate. |
|
||||
| 2.17 | Bug fix for reporting activity metrics that are not found on some devices. |
|
||||
| 2.18 | Bug fix for reporting activity metrics that might be `null` sometimes. This is unsimulatable situation, so this version is a change based on an informed guess. |
|
||||
| 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. |
|
||||
|
@ -274,7 +274,7 @@ Its obvious that a toggle menu item has been triggered as the visible switch cha
|
||||
|
||||
<img src="images/SimTapResponse.png" width="400" title="Tap Triggered"/>
|
||||
|
||||
The application will display a 'toast' showing Home Assistant's friendly name of the triggered item. The toast will disappear after a short while if not dismissed by the user.
|
||||
The application will display a 'toast' showing Home Assistant's friendly name of the triggered item. The toast will disappear after a short while if not dismissed by the user. N.B. There are reports that on the Forerunner 55 device, the toasts do not dissapear without manual intervention. On other devices like the Venu 2 the toast can take 15 seconds to dissappear if not dismissed. Unfortunately, there is no API call to change this behaviour.
|
||||
|
||||
## External Device Changes
|
||||
|
||||
@ -321,6 +321,6 @@ The `id` attribute values are taken from the same names used in [`strings.xml`](
|
||||
|
||||
5. Parameters to tap menu items cannot have their parameter usage verified. If you get this wrong and crash the application, that's your fault not the application's. In this case, start by removing the parameters for the menu item causing the crash, and add them back one at a time until you find your fault. **Please don't give the application a poor review for your bad parameter definition!**
|
||||
|
||||
6. We are unable to support Edge 540 and Edge 840 devices at this time. The simulation of both these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed.
|
||||
6. We are unable to support Edge 540, Edge 840 and Edge 1050 devices at this time. The simulation of these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed.
|
||||
|
||||
7. We are unable to support HTTP without HTTPS. This is a limitation placed upon us by the Connect IQ API which for security reasons refuses to work with HTTP requests. There is nothing developers can do about this limitation. You will have to put an HTTPS proxy in front of your local Home Assistant to work with this application. See the [Trouble Shooting](TroubleShooting.md#do-it-yourself-setup) guide for an example setup. We would appreciate it if users did not leave poor reviews for the lack of this feature which is beyond our control to fix.
|
||||
|
@ -284,3 +284,31 @@ JSON for copy & paste:
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# Debug Logs
|
||||
|
||||
As a desperate measure to assist with debugging the Home Assistant Application, you might be asked to send the authors a debug log.
|
||||
|
||||

|
||||
|
||||
The figure above shows how to find the file on Windows by attaching your watch by USB cable. Inside the `CIQ_LOG.YML` file there are often multiple entries, each looking like this:
|
||||
|
||||
```
|
||||
Error: Unexpected Type Error
|
||||
Details: 'Failed invoking <symbol>'
|
||||
Time: 2024-08-30T12:00:25Z
|
||||
Part-Number: 006-B3703-00
|
||||
Firmware-Version: '19.05'
|
||||
Language-Code: eng
|
||||
ConnectIQ-Version: 4.2.4
|
||||
Store-Id: 61c91d28-ec5e-438d-9f83-39e9f45b199d
|
||||
Store-Version: 30
|
||||
Filename: DCRL0437
|
||||
Appname: HomeAssistant
|
||||
Stack:
|
||||
- pc: 0x10003b5e
|
||||
```
|
||||
|
||||
The only useful information we can glean from this log is the first line, `Error: Unexpected Type Error`. There is no useful mapping to a line of code unless someone can explain to us how to use the `pc` line. Being able to send us the error type does serve as a clue.
|
||||
|
||||
More on [debugging Monkey C applications](https://developer.garmin.com/connect-iq/core-topics/debugging/#appcrashes). The filenames and line numbers must only be present for deployment of code instrumented for debug.
|
||||
|
@ -22,14 +22,16 @@
|
||||
"$ref": "#/$defs/entity"
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "toggle"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a toggle."
|
||||
},
|
||||
"tap_action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -55,17 +57,17 @@
|
||||
"description": "Use 'tap_action' instead to mirror Home Assistant."
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"content": {
|
||||
"title": "What to display (template)",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
|
||||
"const": "template"
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "template",
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'tap' instead."
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
@ -78,17 +80,17 @@
|
||||
"$ref": "#/$defs/entity"
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"content": {
|
||||
"title": "What to display (template)",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
|
||||
"const": "template"
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "template",
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'tap' instead."
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
@ -106,14 +108,16 @@
|
||||
"$ref": "#/$defs/entity"
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "tap"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a tap."
|
||||
},
|
||||
"service": {
|
||||
"$ref": "#/$defs/entity",
|
||||
"deprecated": true,
|
||||
@ -124,14 +128,7 @@
|
||||
"$ref": "#/$defs/tap_action"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["name", "type", "service"]
|
||||
},
|
||||
{
|
||||
"required": ["name", "type", "tap_action"]
|
||||
}
|
||||
],
|
||||
"required": ["name", "type"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"group": {
|
||||
@ -153,10 +150,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "group"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content",
|
||||
"description": "Optional in a group."
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/$defs/items"
|
||||
}
|
||||
@ -164,6 +164,10 @@
|
||||
"required": ["name", "title", "type", "items"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'tap', 'template', 'toggle' or 'group'."
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"maxItems": 16,
|
||||
@ -184,6 +188,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"title": "Your familiar name",
|
||||
"type": "string"
|
||||
},
|
||||
"entity": {
|
||||
"type": "string",
|
||||
"title": "Home Assistant entity name",
|
||||
@ -213,6 +221,10 @@
|
||||
},
|
||||
"required": ["service"]
|
||||
},
|
||||
"content": {
|
||||
"title": "Jinja2 template defining the text to display.",
|
||||
"type": "string"
|
||||
},
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Actions
|
||||
|
||||
A simple example using a scene as a `tap`` menu item.
|
||||
A simple example using a scene as a `tap` menu item.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -43,6 +43,17 @@ Then you can use the following in your config:
|
||||
}
|
||||
```
|
||||
|
||||
And you can optionally include a template to reflect some status. See [Templates](Templates.md) for details on hwo to use this JSON field.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "switch.<switch-name>",
|
||||
"name": "<name>",
|
||||
"type": "toggle",
|
||||
"content": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Example - Covers
|
||||
|
||||
```yaml
|
||||
|
@ -10,6 +10,9 @@ In order to provide the most functionality possible the content of the menu item
|
||||
- `{{` ... `}}` for Expressions to print to the template output
|
||||
- `{#` ... `#}` for Comments not included in the template output
|
||||
|
||||
> [!IMPORTANT]
|
||||
> In order to avoid "Template Error" being displayed as the return value, make sure your Jinja2 template returns a `string`, not a number of some variety. _All numbers must be formatted to strings_ so the application does not need to distinguish an `integer` from a `float`.
|
||||
|
||||
## States
|
||||
|
||||
In this example we get the battery level of the device and add the percent sign. *Very simple*
|
||||
@ -115,6 +118,20 @@ Note: Only when you use the `tap_action` field do you also need to include the `
|
||||
}
|
||||
```
|
||||
|
||||
## Group and Toggle Menu Items
|
||||
|
||||
Both `group` and `toggle` menu items accept an optional `content` field as of v2.19. This allows the use of templates to present status information.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Each Lounge Light",
|
||||
"title": "Lounge",
|
||||
"type": "group",
|
||||
"content": "{{'On: %d, Off: %d'|format(expand(state_attr('light.living_room_lights', 'entity_id'))|selectattr('state','eq','on')|map(attribute='entity_id')|list|count, expand(state_attr('light.living_room_lights', 'entity_id'))|selectattr('state','eq','off')|map(attribute='entity_id')|list|count)}}",
|
||||
"items": [..]
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
Here we generate a bar graph of the battery level. We use the following steps to do this:
|
||||
|
BIN
images/debug_log_location.png
Normal file
BIN
images/debug_log_location.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
21
manifest.xml
21
manifest.xml
@ -54,6 +54,7 @@
|
||||
<iq:product id="edge1030bontrager"/>
|
||||
<iq:product id="edge1030plus"/>
|
||||
<iq:product id="edge1040"/>
|
||||
<iq:product id="edge1050"/>
|
||||
<iq:product id="edge520plus"/>
|
||||
<iq:product id="edge530"/>
|
||||
<iq:product id="edge540"/>
|
||||
@ -63,6 +64,7 @@
|
||||
<iq:product id="edgeexplore"/>
|
||||
<iq:product id="edgeexplore2"/>
|
||||
<iq:product id="enduro"/>
|
||||
<iq:product id="enduro3"/>
|
||||
<iq:product id="epix2"/>
|
||||
<iq:product id="epix2pro42mm"/>
|
||||
<iq:product id="epix2pro47mm"/>
|
||||
@ -86,7 +88,12 @@
|
||||
<iq:product id="fenix7x"/>
|
||||
<iq:product id="fenix7xpro"/>
|
||||
<iq:product id="fenix7xpronowifi"/>
|
||||
<iq:product id="fenix843mm"/>
|
||||
<iq:product id="fenix847mm"/>
|
||||
<iq:product id="fenix8solar47mm"/>
|
||||
<iq:product id="fenix8solar51mm"/>
|
||||
<iq:product id="fenixchronos"/>
|
||||
<iq:product id="fenixe"/>
|
||||
<iq:product id="fr165"/>
|
||||
<iq:product id="fr165m"/>
|
||||
<iq:product id="fr245"/>
|
||||
@ -161,19 +168,17 @@
|
||||
<iq:languages>
|
||||
<iq:language>ara</iq:language>
|
||||
<iq:language>bul</iq:language>
|
||||
<iq:language>zhs</iq:language>
|
||||
<iq:language>zht</iq:language>
|
||||
<iq:language>hrv</iq:language>
|
||||
<iq:language>ces</iq:language>
|
||||
<iq:language>dan</iq:language>
|
||||
<iq:language>dut</iq:language>
|
||||
<iq:language>deu</iq:language>
|
||||
<iq:language>gre</iq:language>
|
||||
<iq:language>dut</iq:language>
|
||||
<iq:language>eng</iq:language>
|
||||
<iq:language>est</iq:language>
|
||||
<iq:language>fin</iq:language>
|
||||
<iq:language>fre</iq:language>
|
||||
<iq:language>gre</iq:language>
|
||||
<iq:language>heb</iq:language>
|
||||
<iq:language>hrv</iq:language>
|
||||
<iq:language>hun</iq:language>
|
||||
<iq:language>ind</iq:language>
|
||||
<iq:language>ita</iq:language>
|
||||
@ -181,13 +186,12 @@
|
||||
<iq:language>kor</iq:language>
|
||||
<iq:language>lav</iq:language>
|
||||
<iq:language>lit</iq:language>
|
||||
<iq:language>zsm</iq:language>
|
||||
<iq:language>nob</iq:language>
|
||||
<iq:language>pol</iq:language>
|
||||
<iq:language>por</iq:language>
|
||||
<iq:language>slo</iq:language>
|
||||
<iq:language>ron</iq:language>
|
||||
<!-- <iq:language>rus</iq:language> -->
|
||||
<iq:language>slo</iq:language>
|
||||
<iq:language>slv</iq:language>
|
||||
<iq:language>spa</iq:language>
|
||||
<iq:language>swe</iq:language>
|
||||
@ -195,6 +199,9 @@
|
||||
<iq:language>tur</iq:language>
|
||||
<iq:language>ukr</iq:language>
|
||||
<iq:language>vie</iq:language>
|
||||
<iq:language>zhs</iq:language>
|
||||
<iq:language>zht</iq:language>
|
||||
<iq:language>zsm</iq:language>
|
||||
</iq:languages>
|
||||
<!--
|
||||
Use "Monkey C: Configure Monkey Barrel" from the Visual Studio Code
|
||||
|
@ -31,8 +31,8 @@ project.manifest = manifest.xml
|
||||
#
|
||||
# The icons need to scale as a ratio of screen size 48:416 pixels
|
||||
#
|
||||
# Icon 53 48 46 42 37 32 30 28 26 24 21 18
|
||||
# Screen 454 416 390 360 320 280 260 240 218 208 176 156
|
||||
# Icon 55 53 48 46 42 37 32 30 28 26 24 21 18
|
||||
# Screen 480 454 416 390 360 320 280 260 240 218 208 176 156
|
||||
|
||||
# Screen Size 390x390 launcher icon size 70x70
|
||||
approachs7042mm.resourcePath = $(approachs7042mm.resourcePath);resources-launcher-70-70;resources-icons-46
|
||||
@ -64,6 +64,8 @@ edge1030bontrager.resourcePath = $(edge1030bontrager.resourcePath);resources-lau
|
||||
edge1030plus.resourcePath = $(edge1030plus.resourcePath);resources-launcher-36-36;resources-icons-32
|
||||
# Screen Size 282x470 launcher icon size 40x40
|
||||
edge1040.resourcePath = $(edge1040.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 480x800 launcher icon size 68x68
|
||||
edge1050.resourcePath = $(edge1050.resourcePath);resources-launcher-68-68;resources-icons-55
|
||||
# Screen Size 200x265 launcher icon size 35x35
|
||||
edge520plus.resourcePath = $(edge520plus.resourcePath);resources-launcher-35-35;resources-icons-24
|
||||
# Screen Size 246x322 launcher icon size 35x35
|
||||
@ -79,6 +81,8 @@ 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
|
||||
# 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
|
||||
@ -108,8 +112,6 @@ fenix7.resourcePath = $(fenix7.resourcePath);resources-launcher-40-40;resources-
|
||||
# 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 218x218 launcher icon size 36x36
|
||||
fenixchronos.resourcePath = $(fenixchronos.resourcePath);resources-launcher-36-36;resources-icons-26
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
fenix7s.resourcePath = $(fenix7s.resourcePath);resources-launcher-40-40;resources-icons-28
|
||||
# Screen Size 240x240 launcher icon size 40x40
|
||||
@ -119,6 +121,18 @@ fenix7x.resourcePath = $(fenix7x.resourcePath);resources-launcher-40-40;resource
|
||||
# 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
|
||||
fenix843mm.resourcePath = $(fenix843mm.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 454x454 launcher icon size 65x65
|
||||
fenix847mm.resourcePath = $(fenix847mm.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 260x260 launcher icon size 40x40
|
||||
fenix8solar47mm.resourcePath = $(fenix8solar47mm.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
fenix8solar51mm.resourcePath = $(fenix8solar51mm.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
# Screen Size 218x218 launcher icon size 36x36
|
||||
fenixchronos.resourcePath = $(fenixchronos.resourcePath);resources-launcher-36-36;resources-icons-26
|
||||
# Screen Size 416x416 launcher icon size 60x60
|
||||
fenixe.resourcePath = $(fenixe.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 390 x 390 launcher icon size 54x54
|
||||
fr165.resourcePath = $(descentmk2s.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
# Screen Size 390 x 390 launcher icon size 54x54
|
||||
|
23
resources-icons-55/drawables.xml
Normal file
23
resources-icons-55/drawables.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
J D Abbey & P A Abbey, 28 December 2022
|
||||
|
||||
References:
|
||||
* https://fonts.google.com/icons
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="ErrorIcon" filename="error.svg"/>
|
||||
<bitmap id="GroupTypeIcon" filename="group_type.svg"/>
|
||||
<bitmap id="TapTypeIcon" filename="tap_type.svg"/>
|
||||
<bitmap id="InfoTypeIcon" filename="info_type.svg"/>
|
||||
</drawables>
|
1
resources-icons-55/error.svg
Normal file
1
resources-icons-55/error.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="55" viewBox="0 0 48 48" width="55" xmlns="http://www.w3.org/2000/svg"><path d="M24 34q.7 0 1.175-.475.475-.475.475-1.175 0-.7-.475-1.175Q24.7 30.7 24 30.7q-.7 0-1.175.475-.475.475-.475 1.175 0 .7.475 1.175Q23.3 34 24 34Zm-1.35-7.65h3V13.7h-3ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 23.95q0-4.1 1.575-7.75 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24.05 4q4.1 0 7.75 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.975T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24.05 41ZM24 24Z" fill="red" stroke="red"/></svg>
|
After Width: | Height: | Size: 713 B |
7
resources-icons-55/group_type.svg
Normal file
7
resources-icons-55/group_type.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg height="55" viewBox="0 0 200 500" width="55" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="0"/>
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="200"/>
|
||||
<rect fill="blue" height="100" rx="40" ry="40" width="200" x="0" y="400"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 362 B |
1
resources-icons-55/info_type.svg
Normal file
1
resources-icons-55/info_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="55" viewBox="0 -960 960 960" width="55" xmlns="http://www.w3.org/2000/svg"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" fill="blue" stroke="blue"/></svg>
|
After Width: | Height: | Size: 543 B |
1
resources-icons-55/tap_type.svg
Normal file
1
resources-icons-55/tap_type.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="55" viewBox="0 -960 960 960" width="55" xmlns="http://www.w3.org/2000/svg"><path d="M445-80q-29 0-56-12t-45-35L127-403l21-23q14-15 34.5-18.5T221-438l99 53v-365q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T380-750v465l-144-77 156 198q10 12 23.76 18 13.76 6 29.24 6h205q38 0 64-26t26-64v-170q0-25.5-17.25-42.75T680-460H460v-60h219.646q50.148 0 85.251 35T800-400v170q0 63-43.5 106.5T650-80H445ZM203-665q-11.074-18.754-17.037-40.492Q180-727.229 180-750.246 180-821 229.725-870.5T350-920q70.55 0 120.275 49.738Q520-820.524 520-749.956q0 22.956-5.963 44.614Q508.074-683.685 497-665l-52-30q7-12 11-26t4-29.478Q460-796 427.882-828q-32.117-32-78-32Q304-860 272-827.917 240-795.833 240-750q0 15 4 29t11 26l-52 30Zm285 335Z" fill="blue" stroke="blue"/></svg>
|
After Width: | Height: | Size: 782 B |
17
resources-launcher-68-68/drawables.xml
Normal file
17
resources-launcher-68-68/drawables.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher.svg" />
|
||||
</drawables>
|
4
resources-launcher-68-68/launcher.svg
Normal file
4
resources-launcher-68-68/launcher.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="68" height="68" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z" fill="#F2F4F9"/>
|
||||
<path d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -56,7 +56,7 @@
|
||||
this delayfor an "always open" mode of operation, which then drains the
|
||||
watch battery from the additional API access activity.
|
||||
-->
|
||||
<property id="poll_delay" type="number">0</property>
|
||||
<property id="poll_delay_combined" type="number">5</property>
|
||||
|
||||
<!--
|
||||
After this time (in seconds), a confirmation dialog for an action is
|
||||
|
@ -66,7 +66,7 @@
|
||||
</setting>
|
||||
|
||||
<setting
|
||||
propertyKey="@Properties.poll_delay"
|
||||
propertyKey="@Properties.poll_delay_combined"
|
||||
title="@Strings.SettingsPollDelay"
|
||||
>
|
||||
<settingConfig type="numeric" min="0" />
|
||||
|
@ -112,7 +112,11 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
data.put("speed", Math.round(position.speed));
|
||||
}
|
||||
if (position.heading != null) {
|
||||
data.put("course", Math.round(position.heading * 180 / Math.PI));
|
||||
var heading = Math.round(position.heading * 180 / Math.PI);
|
||||
while (heading < 0) {
|
||||
heading += 360;
|
||||
}
|
||||
data.put("course", heading);
|
||||
}
|
||||
if (position.altitude != null) {
|
||||
data.put("altitude", Math.round(position.altitude));
|
||||
|
@ -91,7 +91,7 @@ class ErrorView extends ScalableView {
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
static function create(text as Lang.String) as Lang.Array<ErrorView or ErrorDelegate> {
|
||||
static function create(text as Lang.String) as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
|
||||
if (instance == null) {
|
||||
instance = new ErrorView();
|
||||
}
|
||||
@ -116,10 +116,10 @@ class ErrorView extends ScalableView {
|
||||
static function unShow() as Void {
|
||||
if (mShown) {
|
||||
WatchUi.popView(WatchUi.SLIDE_DOWN);
|
||||
// The call to 'updateNextMenuItem()' must be on another thread so that the view is popped above.
|
||||
// The call to 'updateMenuItems()' must be on another thread so that the view is popped above.
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiResume, false);
|
||||
myTimer.start(getApp().method(:updateMenuItems), Globals.scApiResume, false);
|
||||
// This must be last to avoid a race condition with show(), where the
|
||||
// ErrorView can't be dismissed.
|
||||
mShown = false;
|
||||
|
@ -34,12 +34,11 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
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 HomeAssistantTemplateMenuItem> or Null;
|
||||
private var mNextItemToUpdate as Lang.Number = 0; // Index into the above array
|
||||
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 mIsInitUpdateCompl as Lang.Boolean = false;
|
||||
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||
private var mTemplates as Lang.Dictionary = {};
|
||||
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
@ -89,7 +88,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
}
|
||||
|
||||
// Return the initial view of your application here
|
||||
function getInitialView() as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>? {
|
||||
function getInitialView() as [ WatchUi.Views ] or [ WatchUi.Views, WatchUi.InputDelegates ] {
|
||||
mIsApp = true;
|
||||
mQuitTimer = new QuitTimer();
|
||||
mUpdateTimer = new Timer.Timer();
|
||||
@ -119,9 +118,9 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
var isCached = fetchMenuConfig();
|
||||
fetchApiStatus();
|
||||
if (isCached) {
|
||||
return [mHaMenu, new HomeAssistantViewDelegate(true)] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>;
|
||||
return [mHaMenu, new HomeAssistantViewDelegate(true)];
|
||||
} else {
|
||||
return [new WatchUi.View(), new WatchUi.BehaviorDelegate()] as Lang.Array<WatchUi.Views or WatchUi.InputDelegates>;
|
||||
return [new WatchUi.View(), new WatchUi.BehaviorDelegate()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -266,11 +265,124 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
if (mHaMenu != null and !mUpdating) {
|
||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||
// Start the continuous update process that continues for as long as the application is running.
|
||||
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
|
||||
if (mItemsToUpdate.size() > 0) {
|
||||
mUpdating = true;
|
||||
updateNextMenuItemInternal();
|
||||
mTemplates = {};
|
||||
for (var i = 0; i < mItemsToUpdate.size(); i++) {
|
||||
var item = mItemsToUpdate[i];
|
||||
var template = item.buildTemplate();
|
||||
if (template != null) {
|
||||
mTemplates.put(i.toString(), {
|
||||
"template" => template
|
||||
});
|
||||
}
|
||||
if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
mTemplates.put(i.toString() + "t", {
|
||||
"template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate()
|
||||
});
|
||||
}
|
||||
}
|
||||
updateMenuItems();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
break;
|
||||
|
||||
case Communications.BLE_QUEUE_FULL:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(method(:updateMenuItems), Globals.scApiBackoff, false);
|
||||
// Revert status
|
||||
status = getApiStatus();
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 404, page not found. Check API URL setting.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
||||
break;
|
||||
|
||||
case 400:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 400, bad request. Template error.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
break;
|
||||
|
||||
case 200:
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
for (var i = 0; i < mItemsToUpdate.size(); i++) {
|
||||
var item = mItemsToUpdate[i];
|
||||
var state = data.get(i.toString());
|
||||
item.updateState(state);
|
||||
if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
(item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t"));
|
||||
}
|
||||
}
|
||||
var delay = Settings.getPollDelay();
|
||||
if (delay > 0) {
|
||||
mUpdateTimer.start(method(:updateMenuItems), delay, false);
|
||||
} else {
|
||||
updateMenuItems();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// System.println("HomeAssistantApp onReturnUpdateMenuItems(): Unhandled HTTP response code = " + responseCode);
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
}
|
||||
setApiStatus(status);
|
||||
}
|
||||
|
||||
function updateMenuItems() as Void {
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
|
||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else {
|
||||
// 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,
|
||||
{
|
||||
"type" => "render_template",
|
||||
"data" => mTemplates
|
||||
},
|
||||
{
|
||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||
:headers => {
|
||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnUpdateMenuItems)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,11 +432,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
break;
|
||||
|
||||
case 200:
|
||||
var msg = null;
|
||||
if (data != null) {
|
||||
msg = data.get("message");
|
||||
}
|
||||
if (msg.equals("API running.")) {
|
||||
if ((data != null) && data.get("message").equals("API running.")) {
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
} else {
|
||||
if (!mIsGlance) {
|
||||
@ -403,46 +511,14 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
|
||||
function updateNextMenuItem() as Void {
|
||||
var delay = Settings.getPollDelay();
|
||||
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
|
||||
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
|
||||
} else {
|
||||
updateNextMenuItemInternal();
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
function forceStatusUpdates() as Void {
|
||||
// Don't mess with updates unless we are using a timer.
|
||||
if (Settings.getPollDelay() > 0) {
|
||||
mUpdateTimer.stop();
|
||||
mIsInitUpdateCompl = false;
|
||||
// Start from the beginning, or we will only get a partial round of updates before mIsInitUpdateCompl is flipped.
|
||||
mNextItemToUpdate = 0;
|
||||
// For immediate updates
|
||||
updateNextMenuItem();
|
||||
}
|
||||
}
|
||||
|
||||
// We need to spread out the API calls so as not to overload the results queue and cause Communications.BLE_QUEUE_FULL
|
||||
// (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms.
|
||||
function updateNextMenuItemInternal() as Void {
|
||||
var itu = mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem>;
|
||||
if (itu != null) {
|
||||
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
|
||||
itu[mNextItemToUpdate].getState();
|
||||
// mNextItemToUpdate = (mNextItemToUpdate + 1) % itu.size() - But with roll-over detection
|
||||
if (mNextItemToUpdate == itu.size()-1) {
|
||||
// Last item completed return to the start of the list
|
||||
mNextItemToUpdate = 0;
|
||||
mIsInitUpdateCompl = true;
|
||||
} else {
|
||||
mNextItemToUpdate++;
|
||||
}
|
||||
// } else {
|
||||
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
|
||||
updateMenuItems();
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,7 +526,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
return mQuitTimer;
|
||||
}
|
||||
|
||||
function getGlanceView() as Lang.Array<WatchUi.GlanceView or WatchUi.GlanceViewDelegate> or Null {
|
||||
function getGlanceView() as [ WatchUi.GlanceView ] or [ WatchUi.GlanceView, WatchUi.GlanceViewDelegate ] or Null {
|
||||
mIsGlance = true;
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||
@ -475,7 +551,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
|
||||
// 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 Lang.Array<System.ServiceDelegate> {
|
||||
function getServiceDelegate() as [ System.ServiceDelegate ] {
|
||||
return [new BackgroundServiceDelegate()];
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,8 @@
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Menu button with an icon that opens a sub-menu, i.e. group.
|
||||
// Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
|
||||
// a Home Assistant Template.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@ -22,14 +23,17 @@ using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
|
||||
private var mTemplate as Lang.String or Null;
|
||||
private var mMenu as HomeAssistantView;
|
||||
|
||||
function initialize(
|
||||
definition as Lang.Dictionary,
|
||||
template as Lang.String,
|
||||
icon as WatchUi.Drawable,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment
|
||||
} or Null) {
|
||||
} or Null
|
||||
) {
|
||||
|
||||
WatchUi.IconMenuItem.initialize(
|
||||
definition.get("name") as Lang.String,
|
||||
@ -39,11 +43,39 @@ class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
|
||||
options
|
||||
);
|
||||
|
||||
mTemplate = template;
|
||||
mMenu = new HomeAssistantView(definition, null);
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel(null);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantGroupMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function getMenuView() as HomeAssistantView {
|
||||
return mMenu;
|
||||
}
|
||||
|
||||
function hasTemplate() as Lang.Boolean {
|
||||
return mTemplate != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -67,17 +67,19 @@ class HomeAssistantMenuItemFactory {
|
||||
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
|
||||
) as WatchUi.MenuItem {
|
||||
return new HomeAssistantToggleMenuItem(
|
||||
label,
|
||||
template,
|
||||
confirm,
|
||||
{ "entity_id" => entity_id },
|
||||
mMenuItemOptions
|
||||
);
|
||||
}
|
||||
|
||||
function template_tap(
|
||||
function tap(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity as Lang.String or Null,
|
||||
template as Lang.String or Null,
|
||||
@ -92,7 +94,8 @@ class HomeAssistantMenuItemFactory {
|
||||
data.put("entity_id", entity);
|
||||
}
|
||||
}
|
||||
return new HomeAssistantTemplateMenuItem(
|
||||
if (service != null) {
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
template,
|
||||
service,
|
||||
@ -102,50 +105,29 @@ class HomeAssistantMenuItemFactory {
|
||||
mMenuItemOptions,
|
||||
mHomeAssistantService
|
||||
);
|
||||
}
|
||||
|
||||
function template_notap(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String or Null
|
||||
) as WatchUi.MenuItem {
|
||||
return new HomeAssistantTemplateMenuItem(
|
||||
} else {
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
template,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
service,
|
||||
confirm,
|
||||
data,
|
||||
mInfoTypeIcon,
|
||||
mMenuItemOptions,
|
||||
mHomeAssistantService
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function tap(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity as Lang.String or Null,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null
|
||||
function group(
|
||||
definition as Lang.Dictionary,
|
||||
template as Lang.String or Null
|
||||
) as WatchUi.MenuItem {
|
||||
if (entity != null) {
|
||||
if (data == null) {
|
||||
data = { "entity_id" => entity };
|
||||
} else {
|
||||
data.put("entity_id", entity);
|
||||
}
|
||||
}
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
service,
|
||||
confirm,
|
||||
data,
|
||||
mTapTypeIcon,
|
||||
mMenuItemOptions,
|
||||
mHomeAssistantService
|
||||
return new HomeAssistantGroupMenuItem(
|
||||
definition,
|
||||
template,
|
||||
mGroupTypeIcon,
|
||||
mMenuItemOptions
|
||||
);
|
||||
}
|
||||
|
||||
function group(definition as Lang.Dictionary) as WatchUi.MenuItem {
|
||||
return new HomeAssistantGroupMenuItem(definition, mGroupTypeIcon, mMenuItemOptions);
|
||||
}
|
||||
}
|
||||
|
@ -24,12 +24,14 @@ using Toybox.Graphics;
|
||||
|
||||
class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
private var mHomeAssistantService as HomeAssistantService;
|
||||
private var mService as Lang.String;
|
||||
private var mTemplate as Lang.String;
|
||||
private var mService as Lang.String or Null;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mData as Lang.Dictionary or Null;
|
||||
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null,
|
||||
@ -48,11 +50,39 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
);
|
||||
|
||||
mHomeAssistantService = haService;
|
||||
mTemplate = template;
|
||||
mService = service;
|
||||
mConfirm = confirm;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
function hasTemplate() as Lang.Boolean {
|
||||
return mTemplate != null;
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel($.Rez.Strings.Empty);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function callService() as Void {
|
||||
if (mConfirm) {
|
||||
WatchUi.pushView(
|
||||
@ -61,13 +91,15 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
} else {
|
||||
mHomeAssistantService.call(mService, mData);
|
||||
onConfirm(false);
|
||||
}
|
||||
}
|
||||
|
||||
// NB. Parameter 'b' is ignored
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
if (mService != null) {
|
||||
mHomeAssistantService.call(mService, mData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,194 +0,0 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Distributed under MIT Licence
|
||||
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey, 12 January 2024
|
||||
//
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Menu button that renders a Home Assistant Template, and optionally triggers a service.
|
||||
//
|
||||
// Reference:
|
||||
// * https://developers.home-assistant.io/docs/api/rest/
|
||||
// * https://www.home-assistant.io/docs/configuration/templating
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
||||
private var mHomeAssistantService as HomeAssistantService;
|
||||
private var mTemplate as Lang.String;
|
||||
private var mService as Lang.String or Null;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mData as Lang.Dictionary or Null;
|
||||
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
service as Lang.String or Null,
|
||||
confirm as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null,
|
||||
icon as Graphics.BitmapType or WatchUi.Drawable,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment
|
||||
} or Null,
|
||||
haService as HomeAssistantService
|
||||
) {
|
||||
WatchUi.IconMenuItem.initialize(
|
||||
label,
|
||||
null,
|
||||
null,
|
||||
icon,
|
||||
options
|
||||
);
|
||||
|
||||
mHomeAssistantService = haService;
|
||||
mTemplate = template;
|
||||
mService = service;
|
||||
mConfirm = confirm;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
function callService() as Void {
|
||||
if (mConfirm) {
|
||||
WatchUi.pushView(
|
||||
new HomeAssistantConfirmation(),
|
||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
} else {
|
||||
onConfirm(false);
|
||||
}
|
||||
}
|
||||
|
||||
// NB. Parameter 'b' is ignored
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
if (mService != null) {
|
||||
mHomeAssistantService.call(mService, mData);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the status.
|
||||
// Terminate updating the toggle menu items via the chain of calls for a permanent network
|
||||
// error. The ErrorView cancellation will resume the call chain.
|
||||
//
|
||||
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
|
||||
|
||||
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
break;
|
||||
|
||||
case Communications.BLE_QUEUE_FULL:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
|
||||
// Revert status
|
||||
status = getApp().getApiStatus();
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
||||
break;
|
||||
|
||||
case 400:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 400, bad request. Template error.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
break;
|
||||
|
||||
case 200:
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
var label = data.get("request");
|
||||
if (label == null) {
|
||||
setSubLabel($.Rez.Strings.Empty);
|
||||
} else if(label instanceof Lang.String) {
|
||||
setSubLabel(label);
|
||||
} else if(label instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() label = " + label);
|
||||
if (label.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
}
|
||||
requestUpdate();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
getApp().updateNextMenuItem();
|
||||
break;
|
||||
|
||||
default:
|
||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
}
|
||||
getApp().setApiStatus(status);
|
||||
}
|
||||
|
||||
function getState() as Void {
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
|
||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else {
|
||||
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
|
||||
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
|
||||
// System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
|
||||
Communications.makeWebRequest(
|
||||
url,
|
||||
{
|
||||
"type" => "render_template",
|
||||
"data" => {
|
||||
"request" => {
|
||||
"template" => mTemplate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||
:headers => {
|
||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnGetState)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -27,9 +27,12 @@ using Toybox.Timer;
|
||||
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mData as Lang.Dictionary;
|
||||
private var mTemplate as Lang.String;
|
||||
private var mHasVibrate as Lang.Boolean = false;
|
||||
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
confirm as Lang.Boolean,
|
||||
data as Lang.Dictionary or Null,
|
||||
options as {
|
||||
@ -38,128 +41,74 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
} or Null
|
||||
) {
|
||||
WatchUi.ToggleMenuItem.initialize(label, null, null, false, options);
|
||||
if (Attention has :vibrate) {
|
||||
mHasVibrate = true;
|
||||
}
|
||||
mConfirm = confirm;
|
||||
mData = data;
|
||||
mTemplate = template;
|
||||
}
|
||||
|
||||
private function setUiToggle(state as Null or Lang.String) as Void {
|
||||
if (state != null) {
|
||||
if (state.equals("on") && !isEnabled()) {
|
||||
setEnabled(true);
|
||||
WatchUi.requestUpdate();
|
||||
} else if (state.equals("off") && isEnabled()) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildTemplate() as Lang.String or Null {
|
||||
return mTemplate;
|
||||
}
|
||||
function buildToggleTemplate() as Lang.String or Null {
|
||||
return "{{states('" + mData.get("entity_id") + "')}}";
|
||||
}
|
||||
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel(null);
|
||||
} else if(data instanceof Lang.String) {
|
||||
setSubLabel(data);
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
} else {
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
|
||||
if (data == null) {
|
||||
setUiToggle("off");
|
||||
} else if(data instanceof Lang.String) {
|
||||
setUiToggle(data);
|
||||
if (mTemplate == null and data.equals("unavailable")) {
|
||||
setSubLabel($.Rez.Strings.Unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function after completing the GET request to fetch the status.
|
||||
// Terminate updating the toggle menu items via the chain of calls for a permanent network
|
||||
// error. The ErrorView cancellation will resume the call chain.
|
||||
//
|
||||
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode);
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data);
|
||||
|
||||
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||
switch (responseCode) {
|
||||
case Communications.BLE_HOST_TIMEOUT:
|
||||
case Communications.BLE_CONNECTION_UNAVAILABLE:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
break;
|
||||
|
||||
case Communications.BLE_QUEUE_FULL:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
break;
|
||||
|
||||
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
|
||||
var myTimer = new Timer.Timer();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
|
||||
// Revert status
|
||||
status = getApp().getApiStatus();
|
||||
break;
|
||||
|
||||
case 404:
|
||||
var msg = null;
|
||||
if (data != null) {
|
||||
msg = data.get("message");
|
||||
}
|
||||
if (msg != null) {
|
||||
// Should be an HTTP 404 according to curl queries
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404. " + mData.get("entity_id") + " " + msg);
|
||||
ErrorView.show("HTTP 404, " + mData.get("entity_id") + ". " + data.get("message"));
|
||||
} else if(data instanceof Lang.Dictionary) {
|
||||
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
|
||||
if (mTemplate == null) {
|
||||
if (data.get("error") != null) {
|
||||
setSubLabel($.Rez.Strings.TemplateError);
|
||||
} else {
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
||||
setSubLabel($.Rez.Strings.PotentialError);
|
||||
}
|
||||
break;
|
||||
|
||||
case 405:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 405. " + mData.get("entity_id") + " " + data.get("message"));
|
||||
ErrorView.show("HTTP 405, " + mData.get("entity_id") + ". " + data.get("message"));
|
||||
|
||||
break;
|
||||
|
||||
case 200:
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
var state = data.get("state") as Lang.String;
|
||||
// System.println((data.get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
|
||||
if (getLabel().equals("...")) {
|
||||
setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String);
|
||||
}
|
||||
setUiToggle(state);
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
getApp().updateNextMenuItem();
|
||||
break;
|
||||
|
||||
default:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
}
|
||||
getApp().setApiStatus(status);
|
||||
}
|
||||
|
||||
function getState() as Void {
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
|
||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else {
|
||||
var url = Settings.getApiUrl() + "/states/" + mData.get("entity_id");
|
||||
// System.println("HomeAssistantToggleMenuItem getState() URL=" + url);
|
||||
Communications.makeWebRequest(
|
||||
url,
|
||||
null,
|
||||
{
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:headers => {
|
||||
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnGetState)
|
||||
);
|
||||
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
|
||||
if (mTemplate == null) {
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
// Callback function after completing the POST request to set the status.
|
||||
//
|
||||
@ -205,6 +154,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
state = d[i].get("state") as Lang.String;
|
||||
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
|
||||
setUiToggle(state);
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
}
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
@ -218,15 +168,14 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
}
|
||||
|
||||
function setState(s as Lang.Boolean) as Void {
|
||||
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
|
||||
setEnabled(!isEnabled());
|
||||
if (! System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||
// Toggle the UI back
|
||||
setEnabled(!isEnabled());
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
|
||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||
// Toggle the UI back
|
||||
setEnabled(!isEnabled());
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
|
||||
} else {
|
||||
// Updated SDK and got a new error
|
||||
@ -253,6 +202,13 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
},
|
||||
method(:onReturnSetState)
|
||||
);
|
||||
if (mHasVibrate and Settings.getVibrate()) {
|
||||
Attention.vibrate([
|
||||
new Attention.VibeProfile(50, 100), // On for 100ms
|
||||
new Attention.VibeProfile( 0, 100), // Off for 100ms
|
||||
new Attention.VibeProfile(50, 100) // On for 100ms
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,38 +62,40 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
}
|
||||
if (type != null && name != null) {
|
||||
if (type.equals("toggle") && entity != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, confirm));
|
||||
} else if (type.equals("template") && content != null) {
|
||||
if (service == null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().template_notap(name, content));
|
||||
} else {
|
||||
addItem(HomeAssistantMenuItemFactory.create().template_tap(name, entity, content, service, confirm, data));
|
||||
}
|
||||
|
||||
} else if (type.equals("tap") && service != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, service, confirm, data));
|
||||
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm));
|
||||
} else if ((type.equals("tap") && service != null) || (type.equals("template") && content != null)) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, data));
|
||||
} else if (type.equals("group")) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().group(items[i]));
|
||||
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> {
|
||||
// Lang.Array.addAll() fails structural type checking without including "Null" in the return type
|
||||
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> {
|
||||
var fullList = [];
|
||||
|
||||
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
|
||||
|
||||
for(var i = 0; i < mItems.size(); i++) {
|
||||
var item = lmi[i];
|
||||
if (item instanceof HomeAssistantGroupMenuItem) {
|
||||
// Group menu items can now have an optional template to evaluate
|
||||
var gmi = item as HomeAssistantGroupMenuItem;
|
||||
if (gmi.hasTemplate()) {
|
||||
fullList.add(item);
|
||||
}
|
||||
fullList.addAll(item.getMenuView().getItemsToUpdate());
|
||||
} else if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
fullList.add(item);
|
||||
} else if (item instanceof HomeAssistantTemplateMenuItem) {
|
||||
} else if (item instanceof HomeAssistantTapMenuItem) {
|
||||
var tmi = item as HomeAssistantTapMenuItem;
|
||||
if (tmi.hasTemplate()) {
|
||||
fullList.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fullList;
|
||||
}
|
||||
@ -151,10 +153,6 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
|
||||
var haItem = item as HomeAssistantTapMenuItem;
|
||||
// System.println(haItem.getLabel() + " " + haItem.getId());
|
||||
haItem.callService();
|
||||
} else if (item instanceof HomeAssistantTemplateMenuItem) {
|
||||
var haItem = item as HomeAssistantTemplateMenuItem;
|
||||
// System.println(haItem.getLabel() + " " + haItem.getId());
|
||||
haItem.callService();
|
||||
} else if (item instanceof HomeAssistantGroupMenuItem) {
|
||||
var haMenuItem = item as HomeAssistantGroupMenuItem;
|
||||
// System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId());
|
||||
|
@ -57,7 +57,7 @@ class Settings {
|
||||
mClearCache = Properties.getValue("clear_cache");
|
||||
mVibrate = Properties.getValue("enable_vibration");
|
||||
mAppTimeout = Properties.getValue("app_timeout");
|
||||
mPollDelay = Properties.getValue("poll_delay");
|
||||
mPollDelay = Properties.getValue("poll_delay_combined");
|
||||
mConfirmTimeout = Properties.getValue("confirm_timeout");
|
||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||
|
@ -160,26 +160,37 @@ class WebhookManager {
|
||||
|
||||
case 200:
|
||||
case 201:
|
||||
if (data instanceof Lang.Dictionary) {
|
||||
var d = data as Lang.Dictionary;
|
||||
if ((d.get("success") as Lang.Boolean or Null) != false) {
|
||||
var b = d.get("success") as Lang.Boolean or Null;
|
||||
if (b != null and b != false) {
|
||||
if (sensors.size() == 0) {
|
||||
getApp().startUpdates();
|
||||
} else {
|
||||
registerWebhookSensor(sensors);
|
||||
}
|
||||
} else {
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, no 'success'.");
|
||||
Settings.unsetWebhookId();
|
||||
Settings.unsetIsSensorsLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||
}
|
||||
} else {
|
||||
// !! Speculative code for an application crash !!
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, not a Lang.Dict");
|
||||
// Webhook ID might have been deleted on Home Assistant server and a Lang.String is trying to tell us an error message
|
||||
Settings.unsetWebhookId();
|
||||
Settings.unsetIsSensorsLevelEnabled();
|
||||
// ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + data.toString());
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
||||
Settings.unsetWebhookId();
|
||||
Settings.unsetIsSensorsLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + " " + responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,7 +218,6 @@ class WebhookManager {
|
||||
}
|
||||
|
||||
function registerWebhookSensors() {
|
||||
var activityInfo = ActivityMonitor.getInfo();
|
||||
var heartRate = Activity.getActivityInfo().currentHeartRate;
|
||||
|
||||
var sensors = [
|
||||
@ -233,15 +243,6 @@ class WebhookManager {
|
||||
"entity_category" => "diagnostic",
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
},
|
||||
{
|
||||
"name" => "Steps today",
|
||||
"state" => activityInfo.steps == null ? "unknown" : activityInfo.steps,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "steps_today",
|
||||
"icon" => "mdi:walk",
|
||||
"state_class" => "total",
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
},
|
||||
{
|
||||
"name" => "Heart rate",
|
||||
"state" => heartRate == null ? "unknown" : heartRate,
|
||||
@ -254,6 +255,19 @@ class WebhookManager {
|
||||
}
|
||||
];
|
||||
|
||||
if (Toybox has :ActivityMonitor) {
|
||||
System.println("WebhookManager registerWebhookSensors(): has ActivityMonitor class");
|
||||
var activityInfo = ActivityMonitor.getInfo();
|
||||
sensors.add({
|
||||
"name" => "Steps today",
|
||||
"state" => activityInfo.steps == null ? "unknown" : activityInfo.steps,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "steps_today",
|
||||
"icon" => "mdi:walk",
|
||||
"state_class" => "total",
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
});
|
||||
|
||||
if (ActivityMonitor.Info has :floorsClimbed) {
|
||||
sensors.add({
|
||||
"name" => "Floors climbed today",
|
||||
@ -290,6 +304,9 @@ class WebhookManager {
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
System.println("WebhookManager registerWebhookSensors(): has no ActivityMonitor class");
|
||||
}
|
||||
|
||||
if (Activity has :getProfileInfo) {
|
||||
var activity = Activity.getProfileInfo().sport;
|
||||
|
Reference in New Issue
Block a user