mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-09-16 13:41:33 +00:00
Compare commits
38 Commits
v2.30
...
f28a060bf5
Author | SHA1 | Date | |
---|---|---|---|
|
f28a060bf5 | ||
|
5b58a0c1be | ||
|
084e7144cc | ||
|
1ad5cb3263 | ||
|
af3820c7a8 | ||
|
7239bc85c0 | ||
|
6f5e591910 | ||
|
e2722319a6 | ||
|
0b84983eaf | ||
|
d32135af63 | ||
|
db3fbd9886 | ||
|
be7eed1ae1 | ||
|
576f8c4a64 | ||
|
979d85fce5 | ||
|
ac899ff784 | ||
|
b45f02ef7b | ||
|
62f0e711c9 | ||
|
b2b8ffb332 | ||
|
172d4ad1e4 | ||
|
460f247728 | ||
|
a6b4925ff7 | ||
|
3672a598fb | ||
|
f6d0916315 | ||
|
6db7b67536 | ||
|
e5df010af8 | ||
|
ee964ce882 | ||
|
ee9da24592 | ||
|
906cdf7371 | ||
|
7dd85937fa | ||
|
d141c03104 | ||
|
842a31a1cc | ||
|
5639ff5c42 | ||
|
8b3e86a00f | ||
|
6dbcea94cf | ||
|
b28daacafd | ||
|
029a9f373e | ||
|
ec044c5408 | ||
|
659a060c76 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -2,5 +2,8 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"usbs",
|
"usbs",
|
||||||
"Venu"
|
"Venu"
|
||||||
]
|
],
|
||||||
|
"files.exclude": {
|
||||||
|
"resources-*": true
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [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) | [Wi-Fi](Wi-Fi.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||||
|
|
||||||
# Background Service
|
# Background Service
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [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) | [Wi-Fi](Wi-Fi.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||||
|
|
||||||
# Version History
|
# Version History
|
||||||
|
|
||||||
@@ -43,3 +43,6 @@
|
|||||||
| 2.28 | Added support for Vivoactive 6 device which also required an SDK update to 8.1.0. |
|
| 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.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.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. |
|
||||||
|
| 2.32 | Bug fix for a breaking change extracting options caused by the need to rearrange function parameters for an [annoying compiler error](https://github.com/house-of-abbey/GarminHomeAssistant/issues/253). |
|
||||||
|
| 3.0 | First version with the ability to use [Wi-Fi](Wi-Fi.md) instead of Bluetooth but with limited functionality. |
|
||||||
|
17
README.md
17
README.md
@@ -1,4 +1,4 @@
|
|||||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [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) | [Wi-Fi](Wi-Fi.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||||
|
|
||||||
# GarminHomeAssistant
|
# GarminHomeAssistant
|
||||||
|
|
||||||
@@ -10,13 +10,18 @@ The application is designed around a simple scrollable menu where menu items hav
|
|||||||
|
|
||||||
**The intended audience for this application are those comfortable with configuring a Home Assistant** (e.g. editing the YAML configuration files) and debugging why URLs don't work. It does not require programming skills, but the menu is configured via JSON which feels like "coding" (more like "describing"). If you are not comfortable with this relatively low level of configuration, you may like to try other Garmin applications instead.
|
**The intended audience for this application are those comfortable with configuring a Home Assistant** (e.g. editing the YAML configuration files) and debugging why URLs don't work. It does not require programming skills, but the menu is configured via JSON which feels like "coding" (more like "describing"). If you are not comfortable with this relatively low level of configuration, you may like to try other Garmin applications instead.
|
||||||
|
|
||||||
|
**If you are struggling with getting the application to work, please consult the [troubleshooting guide](TroubleShooting.md#menu-configuration-url) first.**
|
||||||
|
|
||||||
|
## No HTTPS?
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> The Garmin SDK allows HTTP requests only to a limited number of domains specified in their app. Therefore, for your Garmin to communicate with your Home Assistant instance, your Home Assistant instance must be accessible via HTTPS (with a public certificate!) or through a local DNS server that overrides one of the whitelisted domains to communicate using HTTP.
|
> The Garmin SDK allows HTTP requests only to a limited number of domains specified in their app. Therefore, for your Garmin to communicate with your Home Assistant instance, your Home Assistant instance must be accessible via HTTPS (with a public certificate!) or through a local DNS server that overrides one of the whitelisted domains to communicate using HTTP.
|
||||||
|
>
|
||||||
> To make your Home Assistant instance accessible via HTTPS, you will need a public certificate. You can get one for free from [Let's Encrypt](https://letsencrypt.org/) or you can pay for [Home Assistant cloud](https://www.nabucasa.com/). (You can install a local [Nginx proxy server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_nginxproxymanager) to manage Let's Encrypt certificates.)
|
> To make your Home Assistant instance accessible via HTTPS, you will need a public certificate. You can get one for free from [Let's Encrypt](https://letsencrypt.org/) or you can pay for [Home Assistant cloud](https://www.nabucasa.com/). (You can install a local [Nginx proxy server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_nginxproxymanager) to manage Let's Encrypt certificates.)
|
||||||
> If you use a local DNS server (like [Pi-Hole](https://pi-hole.net/)), you can create a local DNS record for the domain `garmincdn.com` (which is allowed for HTTP in the Garmin SDK) and map it to your Home Assistant instance's IP. You can find additional workarounds for HTTP request restrictions in the Garmin SDK [here](https://www.instructables.com/About-Communication-Between-Garmin-SDK-and-a-Raspb/).
|
>
|
||||||
|
> If you use a local DNS server (like [Pi-Hole](https://pi-hole.net/)), you can create a local DNS record for the domain `garmincdn.com` (which is allowed for HTTP in the Garmin SDK) and map it to your Home Assistant instance's IP. "_[About Communication Between Garmin SDK and a Raspberry Pi](https://www.instructables.com/About-Communication-Between-Garmin-SDK-and-a-Raspb/)_" provides additional workarounds for HTTP request restrictions in the Garmin SDK.
|
||||||
**If you are struggling with getting the application to work, please consult the [trouble shooting](TroubleShooting.md#menu-configuration-url) guide first.**
|
>
|
||||||
|
> **No support is offered to those circumventing the HTTPS restriction of the Connect IQ SDK.** You are supporting yourself!
|
||||||
|
|
||||||
## Widget or Application?
|
## Widget or Application?
|
||||||
|
|
||||||
@@ -266,7 +271,7 @@ The application timeout prevents the HomeAssistant App running on your watch whe
|
|||||||
| 0 | > 0 | Permanently open, but poll more gently to save battery. |
|
| 0 | > 0 | Permanently open, but poll more gently to save battery. |
|
||||||
| > 0 | > 0 | Temporarily open, poll more gently to save battery, but the application closes before the benefit is realised. Not recommended. |
|
| > 0 | > 0 | Temporarily open, poll more gently to save battery, but the application closes before the benefit is realised. Not recommended. |
|
||||||
|
|
||||||
There is a second timeout value for confirmation views. This is intended for use with more sensitive toggles so that the confirmation view is not left open and forgotten and then confirmed accidentally without you noticing. **We cannot advise you this is safe, be careful what you toggle with the watch application!**
|
There is a second timeout value for confirmation views. This is intended for use with more sensitive toggles so that the confirmation view is not left open and forgotten and then confirmed accidentally without you noticing. **We cannot advise you this is safe, be careful what you toggle with the watch application!** _The developers will not be held responsible for any insecurities resulting from using this feature, including any inadvertent code changes that cause the PIN feature to not work._
|
||||||
|
|
||||||
The confirmation timeout is also used for the maximum time between clicks in the PIN confirmation dialog. The PIN confirmation provides a more secure alternative for toggling security-sensitive actions.
|
The confirmation timeout is also used for the maximum time between clicks in the PIN confirmation dialog. The PIN confirmation provides a more secure alternative for toggling security-sensitive actions.
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [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) | [Wi-Fi](Wi-Fi.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||||
|
|
||||||
# Troubleshooting Guides
|
# Troubleshooting Guides
|
||||||
|
|
||||||
|
33
Wi-Fi.md
Normal file
33
Wi-Fi.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[Home](README.md) | [Switches](examples/Switches.md) | [Actions](examples/Actions.md) | [Templates](examples/Templates.md) | [Glance](examples/Glance.md) | [Background Service](BackgroundService.md) | [Wi-Fi](Wi-Fi.md) | [Trouble Shooting](TroubleShooting.md) | [Version History](HISTORY.md)
|
||||||
|
|
||||||
|
# Wi-Fi & LTE
|
||||||
|
|
||||||
|
Many watches now include the ability to synchronise data over Wi-Fi or event LTE in addition to Bluetooth. This gives users of this application the expectation that they should be able to operate Home Assistant devices from their watch without Bluetooth and hence their phone (that they left out of contact distance). The whole point of Bluetooth after all is that it is [low power](https://en.wikipedia.org/wiki/Bluetooth#Uses). Using Wi-Fi and LTE are power hungry and therefore not something that can be left on continuously in a small device. The watch function that uses Wi-Fi & LTE is the ability to 'synchronise', e.g. activity data (FIT files) and application updates. This function then has a limited period of time for which radio is active. Neither Wi-Fi nor LTE are "always on" like Bluetooth.
|
||||||
|
|
||||||
|
With version 3.0 onwards the application now includes the ability to temporarily turn on Wi-Fi or LTE in order to perform a task on the watch. To do this, the "synchronise" function of the Connect IQ SDK has been cleverly hijacked. This appears to be a highly sought after solution from several users as **it allows the watch to operate when out of range of the associated phone**.
|
||||||
|
|
||||||
|
## Limits of Use
|
||||||
|
|
||||||
|
1. An API request issued over Wi-Fi requires the watch to open up an IP connection to your Wi-Fi access point. This means setting up a secure channel with WPA and being allocated an IP address. Establishing the communication channel takes a short while. _You will see that this adds a noticeable delay to usability._
|
||||||
|
|
||||||
|
2. **The Wi-Fi/LTE functionality can only be used when the menu is already cached.** _The watch will not perform an HTTPS GET request to retrieve the JSON menu file_. Therefore, to enable the Wifi/LTE functionality in the application settings, you must enable caching first.
|
||||||
|
|
||||||
|
3. The menu item statuses will not be set correctly. Instead you will be warned about the lack of connectivity by a 'toast', i.e. message partially occupying the top of the screen temporarily. Fetching the menu item statuses, including rendered templates, requires its own API call, hence this not performed.
|
||||||
|
|
||||||
|
4. Remember that you need to be within range of your watch's configured Wi-Fi access point to utilize this functionality. If supported by your device, LTE offers a longer range, but network charges may apply.
|
||||||
|
|
||||||
|
## Video
|
||||||
|
|
||||||
|
This video using will hopefully make it obvious how slow it is to use the Wi-Fi option and illustrate the cautionary notes above.
|
||||||
|
|
||||||
|
<video width="100%" controls style="max-width:250px">
|
||||||
|
<source src="./images/wi-fi.mp4" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
### Please Note
|
||||||
|
|
||||||
|
We emphasize that the Wi-Fi/LTE functionality should be viewed as a 'last resort' method for executing tasks when your phone is not available. It is not recommended as a continuous mode of operation.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
With thanks to [@vincentezw](https://github.com/vincentezw) for contributing this solution.
|
@@ -47,6 +47,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"$ref": "#/$defs/exit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["entity", "name", "type"],
|
"required": ["entity", "name", "type"],
|
||||||
@@ -75,6 +81,9 @@
|
|||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
"title": "Schema change:",
|
"title": "Schema change:",
|
||||||
"description": "Use 'info' or 'tap' instead."
|
"description": "Use 'info' or 'tap' instead."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "content", "type"],
|
"required": ["name", "content", "type"],
|
||||||
@@ -101,6 +110,12 @@
|
|||||||
},
|
},
|
||||||
"tap_action": {
|
"tap_action": {
|
||||||
"$ref": "#/$defs/tap_action"
|
"$ref": "#/$defs/tap_action"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"$ref": "#/$defs/exit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "content", "type", "tap_action"],
|
"required": ["name", "content", "type", "tap_action"],
|
||||||
@@ -120,6 +135,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"$ref": "#/$defs/type",
|
"$ref": "#/$defs/type",
|
||||||
"const": "info"
|
"const": "info"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "content", "type"],
|
"required": ["name", "content", "type"],
|
||||||
@@ -149,6 +167,12 @@
|
|||||||
},
|
},
|
||||||
"tap_action": {
|
"tap_action": {
|
||||||
"$ref": "#/$defs/tap_action"
|
"$ref": "#/$defs/tap_action"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"$ref": "#/$defs/exit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "type"],
|
"required": ["name", "type"],
|
||||||
@@ -181,6 +205,9 @@
|
|||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/items"
|
"$ref": "#/$defs/items"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"$ref": "#/$defs/enabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "title", "type", "items"],
|
"required": ["name", "title", "type", "items"],
|
||||||
@@ -293,6 +320,18 @@
|
|||||||
"required": ["type"]
|
"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,4 @@
|
|||||||
[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)
|
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||||
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
@@ -76,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.
|
> 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.
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[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)
|
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||||
|
|
||||||
# Glance
|
# Glance
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
[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)
|
[Home](../README.md) | [Switches](Switches.md) | [Actions](Actions.md) | [Templates](Templates.md) | [Glance](Glance.md) | [Background Service](../BackgroundService.md) | [Wi-Fi](../Wi-Fi.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||||
|
|
||||||
|
|
||||||
# Switches
|
# Switches
|
||||||
|
|
||||||
@@ -109,3 +108,29 @@ Then you can use the following in your config:
|
|||||||
"type": "toggle"
|
"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](Templates.md) | [Glance](Glance.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) | [Wi-Fi](../Wi-Fi.md) | [Trouble Shooting](../TroubleShooting.md) | [Version History](../HISTORY.md)
|
||||||
|
|
||||||
# Templates
|
# 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
|
## 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.
|
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.
|
||||||
|
BIN
images/Venu2_Glance_good.png
Normal file
BIN
images/Venu2_Glance_good.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
images/wi-fi.mp4
Normal file
BIN
images/wi-fi.mp4
Normal file
Binary file not shown.
@@ -33,6 +33,7 @@
|
|||||||
"Monkey C: Edit Products" - Lets you add or remove any product
|
"Monkey C: Edit Products" - Lets you add or remove any product
|
||||||
-->
|
-->
|
||||||
<iq:products>
|
<iq:products>
|
||||||
|
<!--
|
||||||
<iq:product id="approachs50"/>
|
<iq:product id="approachs50"/>
|
||||||
<iq:product id="approachs7042mm"/>
|
<iq:product id="approachs7042mm"/>
|
||||||
<iq:product id="approachs7047mm"/>
|
<iq:product id="approachs7047mm"/>
|
||||||
@@ -142,7 +143,9 @@
|
|||||||
<iq:product id="marqgolfer"/>
|
<iq:product id="marqgolfer"/>
|
||||||
<iq:product id="montana7xx"/>
|
<iq:product id="montana7xx"/>
|
||||||
<iq:product id="venu"/>
|
<iq:product id="venu"/>
|
||||||
|
-->
|
||||||
<iq:product id="venu2"/>
|
<iq:product id="venu2"/>
|
||||||
|
<!--
|
||||||
<iq:product id="venu2plus"/>
|
<iq:product id="venu2plus"/>
|
||||||
<iq:product id="venu2s"/>
|
<iq:product id="venu2s"/>
|
||||||
<iq:product id="venu3"/>
|
<iq:product id="venu3"/>
|
||||||
@@ -159,6 +162,7 @@
|
|||||||
<iq:product id="vivoactive4s"/>
|
<iq:product id="vivoactive4s"/>
|
||||||
<iq:product id="vivoactive5"/>
|
<iq:product id="vivoactive5"/>
|
||||||
<iq:product id="vivoactive6"/>
|
<iq:product id="vivoactive6"/>
|
||||||
|
-->
|
||||||
</iq:products>
|
</iq:products>
|
||||||
<!--
|
<!--
|
||||||
Use "Monkey C: Edit Permissions" from the Visual Studio Code command
|
Use "Monkey C: Edit Permissions" from the Visual Studio Code command
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
Poll delay adds a user configurable delay (in seconds) to each round of
|
Poll delay adds a user configurable delay (in seconds) to each round of
|
||||||
status updates of all item in the device's menu that might be amended
|
status updates of all item in the device's menu that might be amended
|
||||||
externally from the watch. A user has requested that it is possible to add
|
externally from the watch. A user has requested that it is possible to add
|
||||||
this delayfor an "always open" mode of operation, which then drains the
|
this delay for an "always open" mode of operation, which then drains the
|
||||||
watch battery from the additional API access activity.
|
watch battery from the additional API access activity.
|
||||||
-->
|
-->
|
||||||
<property id="poll_delay_combined" type="number">5</property>
|
<property id="poll_delay_combined" type="number">5</property>
|
||||||
@@ -96,4 +96,9 @@
|
|||||||
-->
|
-->
|
||||||
<property id="webhook_id" type="string"></property>
|
<property id="webhook_id" type="string"></property>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enables the SyncDelegate and prompt to send a command over Wifi/LTE.
|
||||||
|
This will only show when not connected to the user's phone.
|
||||||
|
-->
|
||||||
|
<property id="wifi_lte_execution" type="boolean">false</property>
|
||||||
</properties>
|
</properties>
|
||||||
|
@@ -116,4 +116,13 @@
|
|||||||
>
|
>
|
||||||
<settingConfig type="alphaNumeric" readonly="true" />
|
<settingConfig type="alphaNumeric" readonly="true" />
|
||||||
</setting>
|
</setting>
|
||||||
|
|
||||||
|
<group enableIfTrue="@Properties.cache_config" id="wifiLteExection" title="@Strings.WifiLteExecution" description="@Strings.WifiLteExecutionDescription">
|
||||||
|
<setting
|
||||||
|
propertyKey="@Properties.wifi_lte_execution"
|
||||||
|
title="@Strings.WifiLteExecutionEnable"
|
||||||
|
>
|
||||||
|
<settingConfig type="boolean" />
|
||||||
|
</setting>
|
||||||
|
</group>
|
||||||
</settings>
|
</settings>
|
||||||
|
@@ -31,7 +31,9 @@
|
|||||||
<string id="NoInternet">No Internet connection.</string>
|
<string id="NoInternet">No Internet connection.</string>
|
||||||
<string id="NoJson">No JSON returned from HTTP request.</string>
|
<string id="NoJson">No JSON returned from HTTP request.</string>
|
||||||
<string id="NoPhone" scope="glance">No Phone connection.</string>
|
<string id="NoPhone" scope="glance">No Phone connection.</string>
|
||||||
|
<string id="NoPhoneNoCache" scope="glance">No phone connection, no cached menu.</string>
|
||||||
<string id="NoResponse">No Response, check Internet connection</string>
|
<string id="NoResponse">No Response, check Internet connection</string>
|
||||||
|
<string id="TimedOut">Request timed out</string>
|
||||||
<string id="PinInputLocked">PIN input locked for</string>
|
<string id="PinInputLocked">PIN input locked for</string>
|
||||||
<string id="PotentialError">Potential Error</string>
|
<string id="PotentialError">Potential Error</string>
|
||||||
<string id="Seconds">seconds</string>
|
<string id="Seconds">seconds</string>
|
||||||
@@ -42,6 +44,10 @@
|
|||||||
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
|
<string id="UnhandledHttpErr">HTTP request returned error code = </string>
|
||||||
<string id="WebhookFailed">Failed to register Webhook</string>
|
<string id="WebhookFailed">Failed to register Webhook</string>
|
||||||
<string id="WrongPin">Wrong PIN</string>
|
<string id="WrongPin">Wrong PIN</string>
|
||||||
|
<string id="WifiLteNotAvailable">No Wifi or LTE available</string>
|
||||||
|
<string id="WifiLtePrompt">Execute over Wifi/LTE?</string>
|
||||||
|
<string id="WifiLteExecutionTitle">Sending to Home Assistant.</string>
|
||||||
|
<string id="WifiLteExecutionDataError">No data received.</string>
|
||||||
|
|
||||||
<!-- For the settings GUI, strings should be in the order they are used. -->
|
<!-- For the settings GUI, strings should be in the order they are used. -->
|
||||||
<string id="SettingsSelect">Select...</string>
|
<string id="SettingsSelect">Select...</string>
|
||||||
@@ -64,4 +70,7 @@
|
|||||||
<string id="SettingsEnableBatteryLevel">Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant.</string>
|
<string id="SettingsEnableBatteryLevel">Enable the background service to send the device battery level, location and (if supported) activity data to Home Assistant.</string>
|
||||||
<string id="SettingsBatteryLevelRefreshRate">The refresh rate (in minutes) at which the background service should repeat sending data.</string>
|
<string id="SettingsBatteryLevelRefreshRate">The refresh rate (in minutes) at which the background service should repeat sending data.</string>
|
||||||
<string id="WebhookId">(Read only) The Webhook ID created by the device for background service updates. You might require this for debugging.</string>
|
<string id="WebhookId">(Read only) The Webhook ID created by the device for background service updates. You might require this for debugging.</string>
|
||||||
|
<string id="WifiLteExecution">Wifi/LTE execution mode.</string>
|
||||||
|
<string id="WifiLteExecutionEnable">Enable executing commands over Wifi/LTE.</string>
|
||||||
|
<string id="WifiLteExecutionDescription">Allows the app to start without phone connection (when menu is cached), and prompt to execute command over Wifi/LTE.</string>
|
||||||
</strings>
|
</strings>
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
|
|
||||||
using Toybox.Application;
|
using Toybox.Application;
|
||||||
|
using Toybox.Communications;
|
||||||
using Toybox.Lang;
|
using Toybox.Lang;
|
||||||
using Toybox.WatchUi;
|
using Toybox.WatchUi;
|
||||||
using Toybox.System;
|
using Toybox.System;
|
||||||
@@ -25,6 +26,7 @@ using Toybox.Timer;
|
|||||||
(:glance, :background)
|
(:glance, :background)
|
||||||
class HomeAssistantApp extends Application.AppBase {
|
class HomeAssistantApp extends Application.AppBase {
|
||||||
private var mApiStatus as Lang.String or Null;
|
private var mApiStatus as Lang.String or Null;
|
||||||
|
private var mHasToast as Lang.Boolean = false;
|
||||||
private var mMenuStatus as Lang.String or Null;
|
private var mMenuStatus as Lang.String or Null;
|
||||||
private var mHaMenu as HomeAssistantView or Null;
|
private var mHaMenu as HomeAssistantView or Null;
|
||||||
private var mGlanceTemplate as Lang.String or Null = null;
|
private var mGlanceTemplate as Lang.String or Null = null;
|
||||||
@@ -38,6 +40,9 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
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 mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||||
private var mTemplates as Lang.Dictionary = {};
|
private var mTemplates as Lang.Dictionary = {};
|
||||||
|
private var mNotifiedNoBle as Lang.Boolean = false;
|
||||||
|
|
||||||
|
private const wifiPollDelayMs = 2000;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//
|
//
|
||||||
@@ -105,6 +110,7 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
mUpdateTimer = new Timer.Timer();
|
mUpdateTimer = new Timer.Timer();
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Checking) as Lang.String;
|
||||||
|
mHasToast = WatchUi has :showToast;
|
||||||
Settings.update();
|
Settings.update();
|
||||||
|
|
||||||
if (Settings.getApiKey().length() == 0) {
|
if (Settings.getApiKey().length() == 0) {
|
||||||
@@ -122,11 +128,14 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
} else if (Settings.getPin() == null) {
|
} else if (Settings.getPin() == null) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
|
// System.println("HomeAssistantApp getInitialView(): Invalid PIN in application settings.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.SettingsPinError) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().phoneConnected) {
|
} else if (! System.getDeviceSettings().phoneConnected and Settings.getWifiLteExecutionEnabled() and ! hasCachedMenu()) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp getInitialView(): No Phone connection, no cached menu, skipping API call.");
|
||||||
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhoneNoCache) as Lang.String);
|
||||||
|
} else if (! System.getDeviceSettings().phoneConnected and ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
// System.println("HomeAssistantApp getInitialView(): No Phone connection and wifi disabled, skipping API call.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! System.getDeviceSettings().connectionAvailable and ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantApp getInitialView(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp getInitialView(): No Internet connection and wifi disabled, skipping API call.");
|
||||||
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
return ErrorView.create(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
var isCached = fetchMenuConfig();
|
var isCached = fetchMenuConfig();
|
||||||
@@ -227,6 +236,20 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Can we use the cached menu?
|
||||||
|
//!
|
||||||
|
//! @return Return true if there's a menu in cache, and if the user has enabled the cache and
|
||||||
|
//! has not requested to have the cache busted.
|
||||||
|
//
|
||||||
|
function hasCachedMenu() as Lang.Boolean {
|
||||||
|
if (Settings.getClearCache() || !Settings.getCacheConfig()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var menu = Storage.getValue("menu") as Lang.Dictionary;
|
||||||
|
return menu != null;
|
||||||
|
}
|
||||||
|
|
||||||
//! Fetch the menu configuration over HTTPS, which might be locally cached.
|
//! 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
|
//! @return Return true if the menu came from the cache, otherwise false. This is because fetching
|
||||||
@@ -246,22 +269,22 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
Settings.unsetClearCache();
|
Settings.unsetClearCache();
|
||||||
}
|
}
|
||||||
if (menu == null) {
|
if (menu == null) {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if (! phoneConnected or ! internetAvailable) {
|
||||||
|
var errorRez = $.Rez.Strings.NoPhone;
|
||||||
|
if (Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
errorRez = $.Rez.Strings.NoPhoneNoCache;
|
||||||
|
} else if (! internetAvailable) {
|
||||||
|
errorRez = $.Rez.Strings.Unavailable;
|
||||||
|
}
|
||||||
// System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchMenuConfig(): No Phone connection, skipping API call.");
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource(errorRez) as Lang.String);
|
||||||
}
|
}
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mMenuStatus = WatchUi.loadResource(errorRez) as Lang.String;
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
|
||||||
// System.println("HomeAssistantApp fetchMenuConfig(): No Internet connection, skipping API call.");
|
|
||||||
if (mIsGlance) {
|
|
||||||
WatchUi.requestUpdate();
|
|
||||||
} else {
|
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
|
||||||
}
|
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
|
||||||
} else {
|
} else {
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
Settings.getConfigUrl(),
|
Settings.getConfigUrl(),
|
||||||
@@ -301,11 +324,11 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
|
|
||||||
//! Start the periodic menu updates for as long as the application is running.
|
//! Start the periodic menu updates for as long as the application is running.
|
||||||
//
|
//
|
||||||
function startUpdates() {
|
function startUpdates() as Void {
|
||||||
if (mHaMenu != null and !mUpdating) {
|
if (mHaMenu != null and !mUpdating) {
|
||||||
// Start the continuous update process that continues for as long as the application is running.
|
// Start the continuous update process that continues for as long as the application is running.
|
||||||
updateMenuItems();
|
|
||||||
mUpdating = true;
|
mUpdating = true;
|
||||||
|
updateMenuItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,15 +433,50 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
//! Construct the GET request to update all menu items.
|
//! Construct the GET request to update all menu items.
|
||||||
//
|
//
|
||||||
function updateMenuItems() as Void {
|
function updateMenuItems() as Void {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
|
||||||
|
// In Wifi/LTE execution mode, we should not show an error page but use a toast instead.
|
||||||
|
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
|
||||||
|
// Notify only once per disconnection cycle
|
||||||
|
if (!mNotifiedNoBle) {
|
||||||
|
var toast = WatchUi.loadResource($.Rez.Strings.NoPhone);
|
||||||
|
if (!connectionAvailable) {
|
||||||
|
toast = WatchUi.loadResource($.Rez.Strings.NoInternet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHasToast) {
|
||||||
|
WatchUi.showToast(toast, null);
|
||||||
|
} else {
|
||||||
|
new Alert({
|
||||||
|
:timeout => Globals.scAlertTimeout,
|
||||||
|
:font => Graphics.FONT_MEDIUM,
|
||||||
|
:text => toast,
|
||||||
|
:fgcolor => Graphics.COLOR_WHITE,
|
||||||
|
:bgcolor => Graphics.COLOR_BLACK
|
||||||
|
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mNotifiedNoBle = true;
|
||||||
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
|
mUpdateTimer.start(method(:startUpdates), wifiPollDelayMs, false);
|
||||||
|
|
||||||
|
mUpdating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! connectionAvailable) {
|
||||||
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
|
mNotifiedNoBle = false;
|
||||||
|
|
||||||
if (mItemsToUpdate == null or mTemplates == null) {
|
if (mItemsToUpdate == null or mTemplates == null) {
|
||||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||||
mTemplates = {};
|
mTemplates = {};
|
||||||
@@ -531,20 +589,27 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
//
|
//
|
||||||
(:glance)
|
(:glance)
|
||||||
function fetchApiStatus() as Void {
|
function fetchApiStatus() as Void {
|
||||||
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var connectionAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
|
||||||
// System.println("API URL = " + Settings.getApiUrl());
|
// System.println("API URL = " + Settings.getApiUrl());
|
||||||
if (Settings.getApiUrl().equals("")) {
|
if (Settings.getApiUrl().equals("")) {
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
if (! mIsGlance && Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! connectionAvailable)) {
|
||||||
|
// System.println("HomeAssistantApp fetchApiStatus(): In-app Wifi mode (No Phone and Internet connection), early return.");
|
||||||
|
return;
|
||||||
|
} else if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantApp fetchApiStatus(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchApiStatus(): No Phone connection, skipping API call.");
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
|
System.println("we here");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
}
|
}
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! connectionAvailable) {
|
||||||
// System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantApp fetchApiStatus(): No Internet connection, skipping API call.");
|
||||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
mApiStatus = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
|
||||||
if (mIsGlance) {
|
if (mIsGlance) {
|
||||||
@@ -785,6 +850,13 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
return mIsApp;
|
return mIsApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Returns a SyncDelegate for this App
|
||||||
|
//!
|
||||||
|
//! @return a SyncDelegate or null
|
||||||
|
//
|
||||||
|
public function getSyncDelegate() as Communications.SyncDelegate? {
|
||||||
|
return new HomeAssistantSyncDelegate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Global function to return the application object.
|
//! Global function to return the application object.
|
||||||
|
@@ -35,19 +35,43 @@ class HomeAssistantConfirmation extends WatchUi.Confirmation {
|
|||||||
//! Delegate to respond to the confirmation request.
|
//! Delegate to respond to the confirmation request.
|
||||||
//
|
//
|
||||||
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
||||||
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
|
private static var mTimer as Timer.Timer or Null;
|
||||||
private var mTimer as Timer.Timer or Null;
|
|
||||||
private var mState as Lang.Boolean;
|
private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
|
||||||
|
private var mState as Lang.Boolean;
|
||||||
|
private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null;
|
||||||
|
private var mConfirmationView as WatchUi.Confirmation;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
|
//!
|
||||||
|
//! @param options A dictionary describing the following options:
|
||||||
|
//! - callback Method to call on confirmation.
|
||||||
|
//! - confirmationView Confirmation the delegate is active for
|
||||||
|
//! - state Wanted state of a toggle button.
|
||||||
|
//! - toggle Optional setEnabled method to untoggle ToggleItem.
|
||||||
//
|
//
|
||||||
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean) {
|
function initialize(options as {
|
||||||
|
:callback as Method(state as Lang.Boolean) as Void,
|
||||||
|
:confirmationView as WatchUi.Confirmation,
|
||||||
|
:state as Lang.Boolean,
|
||||||
|
:toggleMethod as Method(state as Lang.Boolean) or Null,
|
||||||
|
}) {
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
WatchUi.ConfirmationDelegate.initialize();
|
WatchUi.ConfirmationDelegate.initialize();
|
||||||
mConfirmMethod = callback;
|
mConfirmMethod = options[:callback];
|
||||||
mState = state;
|
mConfirmationView = options[:confirmationView];
|
||||||
|
mState = options[:state];
|
||||||
|
mToggleMethod = options[:toggleMethod];
|
||||||
|
|
||||||
var timeout = Settings.getConfirmTimeout(); // ms
|
var timeout = Settings.getConfirmTimeout(); // ms
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
mTimer = new Timer.Timer();
|
if (mTimer == null) {
|
||||||
|
mTimer = new Timer.Timer();
|
||||||
|
}
|
||||||
|
|
||||||
mTimer.start(method(:onTimeout), timeout, true);
|
mTimer.start(method(:onTimeout), timeout, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +88,11 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
|||||||
}
|
}
|
||||||
if (response == WatchUi.CONFIRM_YES) {
|
if (response == WatchUi.CONFIRM_YES) {
|
||||||
mConfirmMethod.invoke(mState);
|
mConfirmMethod.invoke(mState);
|
||||||
|
} else {
|
||||||
|
// Undo the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -71,6 +100,14 @@ class HomeAssistantConfirmationDelegate extends WatchUi.ConfirmationDelegate {
|
|||||||
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
||||||
function onTimeout() as Void {
|
function onTimeout() as Void {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
// Undo the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var getCurrentView = WatchUi.getCurrentView();
|
||||||
|
if (getCurrentView[0] == mConfirmationView) {
|
||||||
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -70,23 +70,27 @@ class HomeAssistantMenuItemFactory {
|
|||||||
//! @param label Menu item label.
|
//! @param label Menu item label.
|
||||||
//! @param entity_id Home Assistant Entity ID (optional)
|
//! @param entity_id Home Assistant Entity ID (optional)
|
||||||
//! @param template Template for Home Assistant to render (optional)
|
//! @param template Template for Home Assistant to render (optional)
|
||||||
//! @param confirm Should this menu item selection be confirmed?
|
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||||
//! @param pin Should this menu item selection request the security PIN?
|
|
||||||
//
|
//
|
||||||
function toggle(
|
function toggle(
|
||||||
label as Lang.String or Lang.Symbol,
|
label as Lang.String or Lang.Symbol,
|
||||||
entity_id as Lang.String or Null,
|
entity_id as Lang.String or Null,
|
||||||
template as Lang.String or Null,
|
template as Lang.String or Null,
|
||||||
confirm as Lang.Boolean,
|
options as {
|
||||||
pin as Lang.Boolean
|
:exit as Lang.Boolean,
|
||||||
|
:confirm as Lang.Boolean,
|
||||||
|
:pin as Lang.Boolean
|
||||||
|
}
|
||||||
) as WatchUi.MenuItem {
|
) 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(
|
return new HomeAssistantToggleMenuItem(
|
||||||
label,
|
label,
|
||||||
template,
|
template,
|
||||||
confirm,
|
|
||||||
pin,
|
|
||||||
{ "entity_id" => entity_id },
|
{ "entity_id" => entity_id },
|
||||||
mMenuItemOptions
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,18 +100,20 @@ class HomeAssistantMenuItemFactory {
|
|||||||
//! @param entity_id Home Assistant Entity ID (optional)
|
//! @param entity_id Home Assistant Entity ID (optional)
|
||||||
//! @param template Template for Home Assistant to render (optional)
|
//! @param template Template for Home Assistant to render (optional)
|
||||||
//! @param service Template for Home Assistant to render (optional)
|
//! @param service Template for Home Assistant to render (optional)
|
||||||
//! @param confirm Should this menu item selection be confirmed?
|
|
||||||
//! @param pin Should this menu item selection request the security PIN?
|
|
||||||
//! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
|
//! @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(
|
function tap(
|
||||||
label as Lang.String or Lang.Symbol,
|
label as Lang.String or Lang.Symbol,
|
||||||
entity_id as Lang.String or Null,
|
entity_id as Lang.String or Null,
|
||||||
template as Lang.String or Null,
|
template as Lang.String or Null,
|
||||||
service as Lang.String or Null,
|
service as Lang.String or Null,
|
||||||
confirm as Lang.Boolean,
|
data as Lang.Dictionary or Null,
|
||||||
pin as Lang.Boolean,
|
options as {
|
||||||
data as Lang.Dictionary or Null
|
:exit as Lang.Boolean,
|
||||||
|
:confirm as Lang.Boolean,
|
||||||
|
:pin as Lang.Boolean
|
||||||
|
}
|
||||||
) as WatchUi.MenuItem {
|
) as WatchUi.MenuItem {
|
||||||
if (entity_id != null) {
|
if (entity_id != null) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
@@ -116,28 +122,28 @@ class HomeAssistantMenuItemFactory {
|
|||||||
data.put("entity_id", entity_id);
|
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) {
|
if (service != null) {
|
||||||
|
options.put(:icon, mTapTypeIcon);
|
||||||
return new HomeAssistantTapMenuItem(
|
return new HomeAssistantTapMenuItem(
|
||||||
label,
|
label,
|
||||||
template,
|
template,
|
||||||
service,
|
service,
|
||||||
confirm,
|
|
||||||
pin,
|
|
||||||
data,
|
data,
|
||||||
mTapTypeIcon,
|
options,
|
||||||
mMenuItemOptions,
|
|
||||||
mHomeAssistantService
|
mHomeAssistantService
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
options.put(:icon, mInfoTypeIcon);
|
||||||
return new HomeAssistantTapMenuItem(
|
return new HomeAssistantTapMenuItem(
|
||||||
label,
|
label,
|
||||||
template,
|
template,
|
||||||
service,
|
null,
|
||||||
confirm,
|
|
||||||
pin,
|
|
||||||
data,
|
data,
|
||||||
mInfoTypeIcon,
|
options,
|
||||||
mMenuItemOptions,
|
|
||||||
mHomeAssistantService
|
mHomeAssistantService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -185,21 +185,25 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
private var mTimer as Timer.Timer or Null;
|
private var mTimer as Timer.Timer or Null;
|
||||||
private var mState as Lang.Boolean;
|
private var mState as Lang.Boolean;
|
||||||
private var mFailures as PinFailures;
|
private var mFailures as PinFailures;
|
||||||
|
private var mToggleMethod as Method(state as Lang.Boolean) as Void or Null;
|
||||||
private var mView as HomeAssistantPinConfirmationView;
|
private var mView as HomeAssistantPinConfirmationView;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//!
|
//!
|
||||||
//! @param callback Method to call on confirmation.
|
//! @param options A dictionary describing the following options:
|
||||||
//! @param state Current state of a toggle button.
|
//! - callback Method to call on confirmation.
|
||||||
//! @param pin PIN to be matched.
|
//! - pin PIN to be matched.
|
||||||
//! @param view PIN confirmation view.
|
//! - state Wanted state of a toggle button.
|
||||||
|
//! - toggle Optional setEnabled method to untoggle ToggleItem.
|
||||||
|
//! - view PIN confirmation view.
|
||||||
//
|
//
|
||||||
function initialize(
|
function initialize(options as {
|
||||||
callback as Method(state as Lang.Boolean) as Void,
|
:callback as Method(state as Lang.Boolean) as Void,
|
||||||
state as Lang.Boolean,
|
:pin as Lang.String,
|
||||||
pin as Lang.String,
|
:state as Lang.Boolean,
|
||||||
view as HomeAssistantPinConfirmationView
|
:view as HomeAssistantPinConfirmationView,
|
||||||
) {
|
:toggleMethod as (Method(state as Lang.Boolean) as Void) or Null,
|
||||||
|
}) {
|
||||||
BehaviorDelegate.initialize();
|
BehaviorDelegate.initialize();
|
||||||
mFailures = new PinFailures();
|
mFailures = new PinFailures();
|
||||||
if (mFailures.isLocked()) {
|
if (mFailures.isLocked()) {
|
||||||
@@ -208,11 +212,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
WatchUi.loadResource($.Rez.Strings.Seconds);
|
WatchUi.loadResource($.Rez.Strings.Seconds);
|
||||||
WatchUi.showToast(msg, {});
|
WatchUi.showToast(msg, {});
|
||||||
}
|
}
|
||||||
mPin = pin;
|
mPin = options[:pin];
|
||||||
mEnteredPin = "";
|
mEnteredPin = "";
|
||||||
mConfirmMethod = callback;
|
mConfirmMethod = options[:callback];
|
||||||
mState = state;
|
mState = options[:state];
|
||||||
mView = view;
|
mToggleMethod = options[:toggleMethod];
|
||||||
|
mView = options[:view];
|
||||||
|
|
||||||
resetTimer();
|
resetTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +243,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
if (mTimer != null) {
|
if (mTimer != null) {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
}
|
}
|
||||||
mConfirmMethod.invoke(mState);
|
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
|
|
||||||
|
// Set the toggle, if we have one
|
||||||
|
if (mToggleMethod != null) {
|
||||||
|
mToggleMethod.invoke(!mState);
|
||||||
|
}
|
||||||
|
mConfirmMethod.invoke(mState);
|
||||||
} else {
|
} else {
|
||||||
error();
|
error();
|
||||||
}
|
}
|
||||||
@@ -279,6 +290,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
if (mTimer != null) {
|
if (mTimer != null) {
|
||||||
mTimer.stop();
|
mTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +316,13 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
|
|||||||
goBack();
|
goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Handle the back button (ESC)
|
||||||
|
//
|
||||||
|
function onBack() as Lang.Boolean {
|
||||||
|
goBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,9 +27,8 @@ class HomeAssistantService {
|
|||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//
|
//
|
||||||
function initialize() {
|
function initialize() {
|
||||||
if (WatchUi has :showToast) {
|
mHasToast = WatchUi has :showToast;
|
||||||
mHasToast = true;
|
|
||||||
}
|
|
||||||
if (Attention has :vibrate) {
|
if (Attention has :vibrate) {
|
||||||
mHasVibrate = true;
|
mHasVibrate = true;
|
||||||
}
|
}
|
||||||
@@ -46,7 +45,13 @@ class HomeAssistantService {
|
|||||||
data as Null or Lang.Dictionary or Lang.String,
|
data as Null or Lang.Dictionary or Lang.String,
|
||||||
context as Lang.Object
|
context as Lang.Object
|
||||||
) as Void {
|
) 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 Code: " + responseCode);
|
||||||
// System.println("HomeAssistantService onReturnCall() Response Data: " + data);
|
// System.println("HomeAssistantService onReturnCall() Response Data: " + data);
|
||||||
|
|
||||||
@@ -102,6 +107,9 @@ class HomeAssistantService {
|
|||||||
:bgcolor => Graphics.COLOR_BLACK
|
:bgcolor => Graphics.COLOR_BLACK
|
||||||
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||||
}
|
}
|
||||||
|
if (exit) {
|
||||||
|
System.exit();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -117,12 +125,28 @@ class HomeAssistantService {
|
|||||||
//
|
//
|
||||||
function call(
|
function call(
|
||||||
service as Lang.String,
|
service as Lang.String,
|
||||||
data as Lang.Dictionary or Null
|
data as Lang.Dictionary or Null,
|
||||||
|
exit as Lang.Boolean
|
||||||
) as Void {
|
) as Void {
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if (Settings.getWifiLteExecutionEnabled() && (! phoneConnected || ! internetAvailable)) {
|
||||||
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
|
WatchUi.pushView(
|
||||||
|
dialog,
|
||||||
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "service",
|
||||||
|
:service => service,
|
||||||
|
:data => data,
|
||||||
|
:exit => exit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
} else if (! phoneConnected) {
|
||||||
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantService call(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! internetAvailable) {
|
||||||
// System.println("HomeAssistantService call(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantService call(): No Internet connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
@@ -149,7 +173,10 @@ class HomeAssistantService {
|
|||||||
"Authorization" => "Bearer " + Settings.getApiKey()
|
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||||
},
|
},
|
||||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
|
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
|
||||||
:context => entity_id
|
:context => {
|
||||||
|
:entity_id => entity_id,
|
||||||
|
:exit => exit
|
||||||
|
}
|
||||||
},
|
},
|
||||||
method(:onReturnCall)
|
method(:onReturnCall)
|
||||||
);
|
);
|
||||||
|
111
source/HomeAssistantSyncDelegate.mc
Normal file
111
source/HomeAssistantSyncDelegate.mc
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using Toybox.Communications;
|
||||||
|
using Toybox.Lang;
|
||||||
|
|
||||||
|
// SyncDelegate to execute single command via POST request to Home Assistant
|
||||||
|
//
|
||||||
|
class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
|
||||||
|
private static var syncError as Lang.String or Null;
|
||||||
|
|
||||||
|
// Initialize an instance of this delegate
|
||||||
|
public function initialize() {
|
||||||
|
SyncDelegate.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Called by the system to determine if a sync is needed
|
||||||
|
public function isSyncNeeded() as Lang.Boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Called by the system when starting a bulk sync.
|
||||||
|
public function onStartSync() as Void {
|
||||||
|
syncError = null;
|
||||||
|
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData == null) {
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionDataError) as Lang.String;
|
||||||
|
onStopSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = WifiLteExecutionConfirmDelegate.mCommandData[:type];
|
||||||
|
var data = WifiLteExecutionConfirmDelegate.mCommandData[:data];
|
||||||
|
var url;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "service":
|
||||||
|
var service = WifiLteExecutionConfirmDelegate.mCommandData[:service];
|
||||||
|
url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length());
|
||||||
|
var entity_id = "";
|
||||||
|
if (data != null) {
|
||||||
|
entity_id = data.get("entity_id");
|
||||||
|
if (entity_id == null) {
|
||||||
|
entity_id = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performRequest(url, data);
|
||||||
|
break;
|
||||||
|
case "entity":
|
||||||
|
url = WifiLteExecutionConfirmDelegate.mCommandData[:url];
|
||||||
|
performRequest(url, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a POST request to Hass with a given payload and URL, and calls haCallback
|
||||||
|
private function performRequest(url as Lang.String, data as Lang.Dictionary or Null) {
|
||||||
|
Communications.makeWebRequest(
|
||||||
|
url,
|
||||||
|
data, // May include {"entity_id": xxxx} for service calls
|
||||||
|
{
|
||||||
|
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||||
|
:headers => {
|
||||||
|
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
|
||||||
|
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||||
|
},
|
||||||
|
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
|
||||||
|
},
|
||||||
|
method(:haCallback)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Handle callback from request
|
||||||
|
public function haCallback(code as Lang.Number, data as Null or Lang.Dictionary) as Void {
|
||||||
|
Communications.notifySyncProgress(100);
|
||||||
|
if (code == 200) {
|
||||||
|
syncError = null;
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData[:type].equals("entity")) {
|
||||||
|
var callbackMethod = WifiLteExecutionConfirmDelegate.mCommandData[:callback];
|
||||||
|
if (callbackMethod != null) {
|
||||||
|
var d = data as Lang.Array;
|
||||||
|
callbackMethod.invoke(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onStopSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(code) {
|
||||||
|
case Communications.NETWORK_REQUEST_TIMED_OUT:
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.TimedOut) as Lang.String;
|
||||||
|
break;
|
||||||
|
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||||
|
syncError = WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String;
|
||||||
|
syncError = "";
|
||||||
|
default:
|
||||||
|
var codeMsg = WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String;
|
||||||
|
syncError = codeMsg + code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onStopSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Clean up
|
||||||
|
public function onStopSync() as Void {
|
||||||
|
if (WifiLteExecutionConfirmDelegate.mCommandData[:exit]) {
|
||||||
|
System.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Communications.cancelAllRequests();
|
||||||
|
Communications.notifySyncComplete(syncError);
|
||||||
|
}
|
||||||
|
}
|
@@ -21,8 +21,9 @@ using Toybox.Graphics;
|
|||||||
//
|
//
|
||||||
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||||
private var mHomeAssistantService as HomeAssistantService;
|
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 mConfirm as Lang.Boolean;
|
||||||
|
private var mExit as Lang.Boolean;
|
||||||
private var mPin as Lang.Boolean;
|
private var mPin as Lang.Boolean;
|
||||||
private var mData as Lang.Dictionary or Null;
|
private var mData as Lang.Dictionary or Null;
|
||||||
|
|
||||||
@@ -31,45 +32,44 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
|||||||
//! @param label Menu item label.
|
//! @param label Menu item label.
|
||||||
//! @param template Menu item template.
|
//! @param template Menu item template.
|
||||||
//! @param service Menu item service.
|
//! @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 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 pin Should the service call be protected with a PIN for some low level of security?
|
||||||
//! @param data Data to supply to the service call.
|
|
||||||
//! @param icon Icon to use for the menu item.
|
//! @param icon Icon to use for the menu item.
|
||||||
//! @param options Menu item options to be passed on.
|
//! @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
|
//! @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.
|
//! one of these objects is created for all menu items to re-use.
|
||||||
//
|
//
|
||||||
function initialize(
|
function initialize(
|
||||||
label as Lang.String or Lang.Symbol,
|
label as Lang.String or Lang.Symbol,
|
||||||
template as Lang.String,
|
template as Lang.String,
|
||||||
service 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,
|
data as Lang.Dictionary or Null,
|
||||||
icon as Graphics.BitmapType or WatchUi.Drawable,
|
|
||||||
options as {
|
options as {
|
||||||
:alignment as WatchUi.MenuItem.Alignment,
|
: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,
|
} or Null,
|
||||||
haService as HomeAssistantService
|
haService as HomeAssistantService
|
||||||
) {
|
) {
|
||||||
if (options != null) {
|
|
||||||
options.put(:icon, icon);
|
|
||||||
} else {
|
|
||||||
options = { :icon => icon };
|
|
||||||
}
|
|
||||||
|
|
||||||
HomeAssistantMenuItem.initialize(
|
HomeAssistantMenuItem.initialize(
|
||||||
label,
|
label,
|
||||||
template,
|
template,
|
||||||
options
|
{
|
||||||
|
:alignment => options.get(:alignment),
|
||||||
|
:icon => options.get(:icon)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
mHomeAssistantService = haService;
|
mHomeAssistantService = haService;
|
||||||
mService = service;
|
mService = service;
|
||||||
mConfirm = confirm;
|
|
||||||
mPin = pin;
|
|
||||||
mData = data;
|
mData = data;
|
||||||
|
mExit = options.get(:exit);
|
||||||
|
mConfirm = options.get(:confirm);
|
||||||
|
mPin = options.get(:pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
|
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry.
|
||||||
@@ -82,16 +82,43 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
|||||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
pinConfirmationView,
|
pinConfirmationView,
|
||||||
new HomeAssistantPinConfirmationDelegate(method(:onConfirm), false, pin, pinConfirmationView),
|
new HomeAssistantPinConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:pin => pin,
|
||||||
|
:state => false,
|
||||||
|
:view => pinConfirmationView,
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (mConfirm) {
|
} else if (mConfirm) {
|
||||||
WatchUi.pushView(
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
new HomeAssistantConfirmation(),
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
);
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
|
WatchUi.pushView(
|
||||||
|
dialog,
|
||||||
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "service",
|
||||||
|
:service => mService,
|
||||||
|
:data => mData,
|
||||||
|
:exit => mExit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var view = new HomeAssistantConfirmation();
|
||||||
|
WatchUi.pushView(
|
||||||
|
view,
|
||||||
|
new HomeAssistantConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:confirmationView => view,
|
||||||
|
:state => false,
|
||||||
|
}),
|
||||||
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onConfirm(false);
|
onConfirm(false);
|
||||||
}
|
}
|
||||||
@@ -103,7 +130,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
|||||||
//
|
//
|
||||||
function onConfirm(b as Lang.Boolean) as Void {
|
function onConfirm(b as Lang.Boolean) as Void {
|
||||||
if (mService != null) {
|
if (mService != null) {
|
||||||
mHomeAssistantService.call(mService, mData);
|
mHomeAssistantService.call(mService, mData, mExit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,30 +22,30 @@ using Toybox.Timer;
|
|||||||
//! Light or switch toggle menu button that calls the API to maintain the up to date state.
|
//! Light or switch toggle menu button that calls the API to maintain the up to date state.
|
||||||
//
|
//
|
||||||
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
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 mData as Lang.Dictionary;
|
||||||
private var mTemplate as Lang.String;
|
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;
|
private var mHasVibrate as Lang.Boolean = false;
|
||||||
|
|
||||||
//! Class Constructor
|
//! Class Constructor
|
||||||
//!
|
//!
|
||||||
//! @param label Menu item label.
|
//! @param label Menu item label.
|
||||||
//! @param template Menu item template.
|
//! @param template Menu item template.
|
||||||
//! @param confirm Should the service call be confirmed to avoid accidental invocation?
|
|
||||||
//! @param pin Should the service call be protected with a PIN for some low level of security?
|
|
||||||
//! @param data Data to supply to the service call.
|
//! @param data Data to supply to the service call.
|
||||||
//! @param options Menu item options to be passed on.
|
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||||
//
|
//
|
||||||
function initialize(
|
function initialize(
|
||||||
label as Lang.String or Lang.Symbol,
|
label as Lang.String or Lang.Symbol,
|
||||||
template as Lang.String,
|
template as Lang.String,
|
||||||
confirm as Lang.Boolean,
|
|
||||||
pin as Lang.Boolean,
|
|
||||||
data as Lang.Dictionary or Null,
|
data as Lang.Dictionary or Null,
|
||||||
options as {
|
options as {
|
||||||
:alignment as WatchUi.MenuItem.Alignment,
|
: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
|
} or Null
|
||||||
) {
|
) {
|
||||||
WatchUi.ToggleMenuItem.initialize(
|
WatchUi.ToggleMenuItem.initialize(
|
||||||
@@ -53,15 +53,19 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
options
|
{
|
||||||
|
:alignment => options.get(:alignment),
|
||||||
|
:icon => options.get(:icon)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (Attention has :vibrate) {
|
if (Attention has :vibrate) {
|
||||||
mHasVibrate = true;
|
mHasVibrate = true;
|
||||||
}
|
}
|
||||||
mConfirm = confirm;
|
|
||||||
mPin = pin;
|
|
||||||
mData = data;
|
mData = data;
|
||||||
mTemplate = template;
|
mTemplate = template;
|
||||||
|
mExit = options.get(:exit);
|
||||||
|
mConfirm = options.get(:confirm);
|
||||||
|
mPin = options.get(:pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Set the state of a toggle menu item.
|
//! Set the state of a toggle menu item.
|
||||||
@@ -194,16 +198,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
case 200:
|
case 200:
|
||||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
||||||
getApp().forceStatusUpdates();
|
getApp().forceStatusUpdates();
|
||||||
var state;
|
|
||||||
var d = data as Lang.Array;
|
var d = data as Lang.Array;
|
||||||
for(var i = 0; i < d.size(); i++) {
|
setToggleStateWithData(d);
|
||||||
if ((d[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) {
|
|
||||||
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;
|
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -212,6 +208,31 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
|
||||||
}
|
}
|
||||||
getApp().setApiStatus(status);
|
getApp().setApiStatus(status);
|
||||||
|
if (mExit) {
|
||||||
|
System.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Handles the response from a Home Assistant service or state call and updates the toggle UI.
|
||||||
|
//!
|
||||||
|
//! @param data An array of dictionaries, each representing a Home Assistant entity state.
|
||||||
|
//
|
||||||
|
function setToggleStateWithData(data as Lang.Array) {
|
||||||
|
// if there's no response body, let's assume that what we did, happened, and flip the toggle
|
||||||
|
if (data.size() == 0) {
|
||||||
|
setEnabled(!isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
for(var i = 0; i < data.size(); i++) {
|
||||||
|
if ((data[i].get("entity_id") as Lang.String).equals(mData.get("entity_id"))) {
|
||||||
|
var state = data[i].get("state") as Lang.String;
|
||||||
|
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
|
||||||
|
setUiToggle(state);
|
||||||
|
WatchUi.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Set the state of the toggle menu item.
|
//! Set the state of the toggle menu item.
|
||||||
@@ -219,25 +240,27 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
//! @param s Boolean indicating the desired state of the toggle switch.
|
//! @param s Boolean indicating the desired state of the toggle switch.
|
||||||
//
|
//
|
||||||
function setState(s as Lang.Boolean) as Void {
|
function setState(s as Lang.Boolean) as Void {
|
||||||
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
setEnabled(!isEnabled());
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
if (! System.getDeviceSettings().phoneConnected) {
|
|
||||||
|
if (! phoneConnected && ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
|
||||||
} else if (! System.getDeviceSettings().connectionAvailable) {
|
} else if (! internetAvailable && ! Settings.getWifiLteExecutionEnabled()) {
|
||||||
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
|
||||||
// Toggle the UI back
|
// Toggle the UI back
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||||
} else {
|
} else {
|
||||||
// Updated SDK and got a new error
|
|
||||||
// ERROR: venu: Cannot find symbol ':substring' on type 'PolyType<Null or $.Toybox.Lang.Object>'.
|
|
||||||
var id = mData.get("entity_id") as Lang.String;
|
var id = mData.get("entity_id") as Lang.String;
|
||||||
var url = Settings.getApiUrl() + "/services/";
|
var url = getUrl(id, s);
|
||||||
if (s) {
|
|
||||||
url = url + id.substring(0, id.find(".")) + "/turn_on";
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
} else {
|
// Undo the toggle
|
||||||
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
setEnabled(!isEnabled());
|
||||||
|
wifiPrompt(s);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
// System.println("HomeAssistantToggleMenuItem setState() URL = " + url);
|
||||||
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
// System.println("HomeAssistantToggleMenuItem setState() entity_id = " + id);
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
@@ -268,21 +291,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
function callService(b as Lang.Boolean) as Void {
|
function callService(b as Lang.Boolean) as Void {
|
||||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||||
if (mPin && hasTouchScreen) {
|
if (mPin && hasTouchScreen) {
|
||||||
|
// Undo the toggle
|
||||||
|
setEnabled(!isEnabled());
|
||||||
|
|
||||||
var pin = Settings.getPin();
|
var pin = Settings.getPin();
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||||
WatchUi.pushView(
|
WatchUi.pushView(
|
||||||
pinConfirmationView,
|
pinConfirmationView,
|
||||||
new HomeAssistantPinConfirmationDelegate(method(:onConfirm), b, pin, pinConfirmationView),
|
new HomeAssistantPinConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:pin => pin,
|
||||||
|
:state => b,
|
||||||
|
:toggleMethod => method(:setEnabled),
|
||||||
|
:view => pinConfirmationView,
|
||||||
|
}),
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (mConfirm) {
|
} else if (mConfirm) {
|
||||||
WatchUi.pushView(
|
// Undo the toggle
|
||||||
new HomeAssistantConfirmation(),
|
setEnabled(!isEnabled());
|
||||||
new HomeAssistantConfirmationDelegate(method(:onConfirm), b),
|
|
||||||
WatchUi.SLIDE_IMMEDIATE
|
var phoneConnected = System.getDeviceSettings().phoneConnected;
|
||||||
);
|
var internetAvailable = System.getDeviceSettings().connectionAvailable;
|
||||||
|
if ((! phoneConnected || ! internetAvailable) && Settings.getWifiLteExecutionEnabled()) {
|
||||||
|
wifiPrompt(b);
|
||||||
|
} else {
|
||||||
|
var confirmationView = new HomeAssistantConfirmation();
|
||||||
|
WatchUi.pushView(
|
||||||
|
confirmationView,
|
||||||
|
new HomeAssistantConfirmationDelegate({
|
||||||
|
:callback => method(:onConfirm),
|
||||||
|
:confirmationView => confirmationView,
|
||||||
|
:state => b,
|
||||||
|
:toggleMethod => method(:setEnabled),
|
||||||
|
}),
|
||||||
|
WatchUi.SLIDE_IMMEDIATE
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onConfirm(b);
|
onConfirm(b);
|
||||||
}
|
}
|
||||||
@@ -296,4 +343,45 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
setState(b);
|
setState(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Displays a confirmation dialog before executing a service call via Wi-Fi/LTE.
|
||||||
|
//!
|
||||||
|
//! @param s Desired state: `true` to turn on, `false` to turn off.
|
||||||
|
//
|
||||||
|
private function wifiPrompt(s as Lang.Boolean) as Void {
|
||||||
|
var id = mData.get("entity_id") as Lang.String;
|
||||||
|
var url = getUrl(id, s);
|
||||||
|
|
||||||
|
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||||
|
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||||
|
WatchUi.pushView(
|
||||||
|
dialog,
|
||||||
|
new WifiLteExecutionConfirmDelegate({
|
||||||
|
:type => "entity",
|
||||||
|
:url => url,
|
||||||
|
:id => id,
|
||||||
|
:data => mData,
|
||||||
|
:callback => method(:setToggleStateWithData),
|
||||||
|
:exit => mExit,
|
||||||
|
}, dialog),
|
||||||
|
WatchUi.SLIDE_LEFT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Constructs a Home Assistant API URL for the given entity and desired state.
|
||||||
|
//!
|
||||||
|
//! @param id The entity ID, e.g., `"switch.kitchen"`.
|
||||||
|
//! @param s Desired state: `true` for "turn_on", `false` for "turn_off".
|
||||||
|
//!
|
||||||
|
//! @return Full service URL string.
|
||||||
|
//
|
||||||
|
private function getUrl(id as Lang.String, s as Lang.Boolean) as Lang.String {
|
||||||
|
var url = Settings.getApiUrl() + "/services/";
|
||||||
|
if (s) {
|
||||||
|
url = url + id.substring(0, id.find(".")) + "/turn_on";
|
||||||
|
} else {
|
||||||
|
url = url + id.substring(0, id.find(".")) + "/turn_off";
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,21 +51,95 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
|||||||
var confirm = false as Lang.Boolean or Null;
|
var confirm = false as Lang.Boolean or Null;
|
||||||
var pin = false as Lang.Boolean or Null;
|
var pin = false as Lang.Boolean or Null;
|
||||||
var data = null as Lang.Dictionary 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) {
|
if (tap_action != null) {
|
||||||
service = tap_action.get("service");
|
service = tap_action.get("service");
|
||||||
confirm = tap_action.get("confirm"); // Optional
|
data = tap_action.get("data"); // Optional
|
||||||
pin = tap_action.get("pin"); // Optional
|
if (tap_action.get("confirm") != null) {
|
||||||
data = tap_action.get("data"); // Optional
|
confirm = tap_action.get("confirm"); // Optional
|
||||||
if (confirm == null) {
|
}
|
||||||
confirm = false;
|
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) {
|
if (type.equals("toggle") && entity != null) {
|
||||||
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm, pin));
|
addItem(HomeAssistantMenuItemFactory.create().toggle(
|
||||||
} else if ((type.equals("tap") && service != null) || (type.equals("info") && content != null) || (type.equals("template") && content != null)) {
|
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".
|
// 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")) {
|
} else if (type.equals("group")) {
|
||||||
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
|
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,7 @@ class Settings {
|
|||||||
private static var mCacheConfig as Lang.Boolean = false;
|
private static var mCacheConfig as Lang.Boolean = false;
|
||||||
private static var mClearCache as Lang.Boolean = false;
|
private static var mClearCache as Lang.Boolean = false;
|
||||||
private static var mVibrate as Lang.Boolean = false;
|
private static var mVibrate as Lang.Boolean = false;
|
||||||
|
private static var mWifiLteExecution as Lang.Boolean = false;
|
||||||
//! seconds
|
//! seconds
|
||||||
private static var mAppTimeout as Lang.Number = 0;
|
private static var mAppTimeout as Lang.Number = 0;
|
||||||
//! seconds
|
//! seconds
|
||||||
@@ -69,6 +70,7 @@ class Settings {
|
|||||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||||
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
||||||
|
mWifiLteExecution = Properties.getValue("wifi_lte_execution");
|
||||||
}
|
}
|
||||||
|
|
||||||
//! A webhook is required for non-privileged API calls.
|
//! A webhook is required for non-privileged API calls.
|
||||||
@@ -270,4 +272,16 @@ class Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Get the value of the WiFi/LTE toggle in settings.
|
||||||
|
//!
|
||||||
|
//! @return The state of the toggle.
|
||||||
|
//
|
||||||
|
static function getWifiLteExecutionEnabled() as Lang.Boolean {
|
||||||
|
// Wifi/LTE sync execution on a cached menu
|
||||||
|
if (!mCacheConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mWifiLteExecution;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
135
source/WifiLteExecutionConfirmDelegate.mc
Normal file
135
source/WifiLteExecutionConfirmDelegate.mc
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Toybox.WatchUi;
|
||||||
|
using Toybox.System;
|
||||||
|
using Toybox.Communications;
|
||||||
|
using Toybox.Lang;
|
||||||
|
using Toybox.Timer;
|
||||||
|
|
||||||
|
// Delegate to respond to a confirmation to execute command via bulk sync
|
||||||
|
//
|
||||||
|
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
|
||||||
|
public static var mCommandData as {
|
||||||
|
:type as Lang.String,
|
||||||
|
:service as Lang.String or Null,
|
||||||
|
:data as Lang.Dictionary or Null,
|
||||||
|
:url as Lang.String or Null,
|
||||||
|
:id as Lang.Number or Null,
|
||||||
|
:exit as Lang.Boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
private static var mTimer as Timer.Timer or Null;
|
||||||
|
private var mHasToast as Lang.Boolean = false;
|
||||||
|
private var mConfirmationView as WatchUi.Confirmation;
|
||||||
|
|
||||||
|
//! Initializes a confirmation delegate to confirm a Wi-Fi or LTE command exection
|
||||||
|
//!
|
||||||
|
//! @param options A dictionary describing the command to be executed:
|
||||||
|
//! - type: The command type, either `"service"` or `"entity"`.
|
||||||
|
//! - service: (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on").
|
||||||
|
//! - url: (For type `"entity"`) The full Home Assistant entity API URL.
|
||||||
|
//! - callback: (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.
|
||||||
|
//! - data: (Optional) A dictionary of data to send with the request.
|
||||||
|
//! - exit: Boolean: if set to true: exit after running command.
|
||||||
|
//! @param view The Confirmation view the delegate is active for
|
||||||
|
function initialize(cOptions as {
|
||||||
|
:type as Lang.String,
|
||||||
|
:service as Lang.String or Null,
|
||||||
|
:data as Lang.Dictionary or Null,
|
||||||
|
:url as Lang.String or Null,
|
||||||
|
:callback as Lang.Method or Null,
|
||||||
|
:exit as Lang.Boolean,
|
||||||
|
}, view as WatchUi.Confirmation) {
|
||||||
|
ConfirmationDelegate.initialize();
|
||||||
|
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WatchUi has :showToast) {
|
||||||
|
mHasToast = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mConfirmationView = view;
|
||||||
|
mCommandData = {
|
||||||
|
:type => cOptions[:type],
|
||||||
|
:service => cOptions[:service],
|
||||||
|
:data => cOptions[:data],
|
||||||
|
:url => cOptions[:url],
|
||||||
|
:callback => cOptions[:callback],
|
||||||
|
:exit => cOptions[:exit]
|
||||||
|
};
|
||||||
|
|
||||||
|
var timeout = Settings.getConfirmTimeout(); // ms
|
||||||
|
if (timeout > 0) {
|
||||||
|
if (mTimer == null) {
|
||||||
|
mTimer = new Timer.Timer();
|
||||||
|
}
|
||||||
|
|
||||||
|
mTimer.start(method(:onTimeout), timeout, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Handles the user's response to the confirmation dialog.
|
||||||
|
//!
|
||||||
|
//! @param response The user's confirmation response as `WatchUi.Confirm`
|
||||||
|
//! @return Always returns `true` to indicate the response was handled.
|
||||||
|
function onResponse(response) as Lang.Boolean {
|
||||||
|
getApp().getQuitTimer().reset();
|
||||||
|
if (mTimer != null) {
|
||||||
|
mTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response == WatchUi.CONFIRM_YES) {
|
||||||
|
trySync();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Initiates a bulk sync process to execute a command, if connections are available
|
||||||
|
private function trySync() as Void {
|
||||||
|
var connectionInfo = System.getDeviceSettings().connectionInfo;
|
||||||
|
var keys = connectionInfo.keys();
|
||||||
|
var possibleConnection = false;
|
||||||
|
|
||||||
|
for(var i = 0; i < keys.size(); i++) {
|
||||||
|
if (keys[i] != :bluetooth) {
|
||||||
|
var connection = connectionInfo[keys[i]];
|
||||||
|
if (connection.state != System.CONNECTION_STATE_NOT_INITIALIZED) {
|
||||||
|
possibleConnection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibleConnection) {
|
||||||
|
if (Communications has :startSync2) {
|
||||||
|
var syncString = WatchUi.loadResource($.Rez.Strings.WifiLteExecutionTitle) as Lang.String;
|
||||||
|
Communications.startSync2({:message => syncString});
|
||||||
|
} else {
|
||||||
|
Communications.startSync();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var toast = WatchUi.loadResource($.Rez.Strings.WifiLteNotAvailable) as Lang.String;
|
||||||
|
if (mHasToast) {
|
||||||
|
WatchUi.showToast(toast, null);
|
||||||
|
} else {
|
||||||
|
new Alert({
|
||||||
|
:timeout => Globals.scAlertTimeout,
|
||||||
|
:font => Graphics.FONT_MEDIUM,
|
||||||
|
:text => toast,
|
||||||
|
:fgcolor => Graphics.COLOR_WHITE,
|
||||||
|
:bgcolor => Graphics.COLOR_BLACK
|
||||||
|
}).pushView(WatchUi.SLIDE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Function supplied to a timer in order to limit the time for which the confirmation can be provided.
|
||||||
|
function onTimeout() as Void {
|
||||||
|
mTimer.stop();
|
||||||
|
var getCurrentView = WatchUi.getCurrentView();
|
||||||
|
|
||||||
|
if (getCurrentView[0] == mConfirmationView) {
|
||||||
|
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -37,7 +37,8 @@
|
|||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
filter: grayscale() invert();
|
filter: grayscale() invert();
|
||||||
}
|
}
|
||||||
.template, .info {
|
.template,
|
||||||
|
.info {
|
||||||
background-image: url(../resources-icons-48/info_type.svg);
|
background-image: url(../resources-icons-48/info_type.svg);
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
@@ -63,6 +64,13 @@
|
|||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
filter: grayscale() invert();
|
filter: grayscale() invert();
|
||||||
}
|
}
|
||||||
|
.mdi {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
@@ -285,6 +293,10 @@
|
|||||||
background-color: var(--ctp-mocha-overlay1);
|
background-color: var(--ctp-mocha-overlay1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css"
|
||||||
|
defer />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="settings">
|
<div id="settings">
|
||||||
@@ -441,9 +453,10 @@ http:
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="https://www.unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
<script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script>
|
||||||
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
|
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
|
||||||
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
|
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
|
||||||
|
<script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>
|
||||||
<script type="module" src="./main.js"></script>
|
<script type="module" src="./main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
70
web/main.js
70
web/main.js
@@ -11,7 +11,7 @@ let api_token = localStorage.getItem('api_token') ?? '';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all entities in HomeAssistant.
|
* Get all entities in HomeAssistant.
|
||||||
* @returns {Promise<Record<string, string>>} [id, name]
|
* @returns {Promise<Record<string, { name: string, icon?: string }>>} [id, name]
|
||||||
*/
|
*/
|
||||||
async function get_entities() {
|
async function get_entities() {
|
||||||
try {
|
try {
|
||||||
@@ -21,7 +21,7 @@ async function get_entities() {
|
|||||||
Authorization: `Bearer ${api_token}`,
|
Authorization: `Bearer ${api_token}`,
|
||||||
},
|
},
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
|
body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\",\\"{{ entity.attributes.icon }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
|
||||||
});
|
});
|
||||||
if (res.status == 401 || res.status == 403) {
|
if (res.status == 401 || res.status == 403) {
|
||||||
document.querySelector('#api_token').classList.add('invalid');
|
document.querySelector('#api_token').classList.add('invalid');
|
||||||
@@ -29,8 +29,16 @@ async function get_entities() {
|
|||||||
}
|
}
|
||||||
document.querySelector('#api_url').classList.remove('invalid');
|
document.querySelector('#api_url').classList.remove('invalid');
|
||||||
document.querySelector('#api_token').classList.remove('invalid');
|
document.querySelector('#api_token').classList.remove('invalid');
|
||||||
return Object.fromEntries(await res.json());
|
const data = {};
|
||||||
} catch {
|
for (const [id, name, icon] of await res.json()) {
|
||||||
|
data[id] = { name };
|
||||||
|
if (icon !== '') {
|
||||||
|
data[id].icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fetching entities:', e);
|
||||||
document.querySelector('#api_url').classList.add('invalid');
|
document.querySelector('#api_url').classList.add('invalid');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -57,7 +65,8 @@ async function get_devices() {
|
|||||||
document.querySelector('#api_url').classList.remove('invalid');
|
document.querySelector('#api_url').classList.remove('invalid');
|
||||||
document.querySelector('#api_token').classList.remove('invalid');
|
document.querySelector('#api_token').classList.remove('invalid');
|
||||||
return Object.fromEntries(await res.json());
|
return Object.fromEntries(await res.json());
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error('Error fetching devices:', e);
|
||||||
document.querySelector('#api_url').classList.add('invalid');
|
document.querySelector('#api_url').classList.add('invalid');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -84,7 +93,8 @@ async function get_areas() {
|
|||||||
document.querySelector('#api_url').classList.remove('invalid');
|
document.querySelector('#api_url').classList.remove('invalid');
|
||||||
document.querySelector('#api_token').classList.remove('invalid');
|
document.querySelector('#api_token').classList.remove('invalid');
|
||||||
return Object.fromEntries(await res.json());
|
return Object.fromEntries(await res.json());
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error('Error fetching areas:', e);
|
||||||
document.querySelector('#api_url').classList.add('invalid');
|
document.querySelector('#api_url').classList.add('invalid');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -119,7 +129,8 @@ async function get_services() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return services;
|
return services;
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error('Error fetching services:', e);
|
||||||
document.querySelector('#api_url').classList.add('invalid');
|
document.querySelector('#api_url').classList.add('invalid');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -370,6 +381,9 @@ async function generate_schema(entities, devices, areas, services, schema) {
|
|||||||
confirm: {
|
confirm: {
|
||||||
$ref: '#/$defs/confirm',
|
$ref: '#/$defs/confirm',
|
||||||
},
|
},
|
||||||
|
pin: {
|
||||||
|
$ref: '#/$defs/pin',
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {},
|
properties: {},
|
||||||
@@ -724,6 +738,7 @@ require(['vs/editor/editor.main'], async () => {
|
|||||||
|
|
||||||
var decorations = editor.createDecorationsCollection([]);
|
var decorations = editor.createDecorationsCollection([]);
|
||||||
|
|
||||||
|
/** @type {monaco.editor.IMarkerData[]} */
|
||||||
let markers = [];
|
let markers = [];
|
||||||
|
|
||||||
const renderTemplate = editor.addCommand(
|
const renderTemplate = editor.addCommand(
|
||||||
@@ -905,6 +920,7 @@ require(['vs/editor/editor.main'], async () => {
|
|||||||
const ast = json.parse(model.getValue());
|
const ast = json.parse(model.getValue());
|
||||||
const data = JSON.parse(model.getValue());
|
const data = JSON.parse(model.getValue());
|
||||||
markers = [];
|
markers = [];
|
||||||
|
/** @type {monaco.editor.IModelDeltaDecoration[]} */
|
||||||
const glyphs = [];
|
const glyphs = [];
|
||||||
async function testToggle(range, entity) {
|
async function testToggle(range, entity) {
|
||||||
const res = await fetch(api_url + '/states/' + entity, {
|
const res = await fetch(api_url + '/states/' + entity, {
|
||||||
@@ -983,8 +999,10 @@ require(['vs/editor/editor.main'], async () => {
|
|||||||
* @param {import('json-ast-comments').JsonAst |
|
* @param {import('json-ast-comments').JsonAst |
|
||||||
* import('json-ast-comments').JsonProperty} node
|
* import('json-ast-comments').JsonProperty} node
|
||||||
* @param {string[]} path
|
* @param {string[]} path
|
||||||
|
* @param {import('json-ast-comments').JsonAst |
|
||||||
|
* import('json-ast-comments').JsonProperty | null} parent
|
||||||
*/
|
*/
|
||||||
function recurse(node, path) {
|
function recurse(node, path, parent = null) {
|
||||||
if (node.type === 'property') {
|
if (node.type === 'property') {
|
||||||
if (node.key[0].value === 'content') {
|
if (node.key[0].value === 'content') {
|
||||||
templates.push([
|
templates.push([
|
||||||
@@ -1010,11 +1028,39 @@ require(['vs/editor/editor.main'], async () => {
|
|||||||
}
|
}
|
||||||
trim++;
|
trim++;
|
||||||
markers.push({
|
markers.push({
|
||||||
message: entities[node.value[0].value] ?? 'Entity not found',
|
message: entities[node.value[0].value].name ?? 'Entity not found',
|
||||||
severity: monaco.MarkerSeverity.Hint,
|
severity: monaco.MarkerSeverity.Hint,
|
||||||
...range,
|
...range,
|
||||||
startColumn: trim,
|
startColumn: trim,
|
||||||
});
|
});
|
||||||
|
glyphs.push({
|
||||||
|
range,
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
glyphMarginClassName:
|
||||||
|
'mdi ' +
|
||||||
|
entities[node.value[0].value]?.icon?.replace(':', '-'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
node.key[0].value === 'enabled' &&
|
||||||
|
node.value[0].type === 'boolean' &&
|
||||||
|
!node.value[0].value
|
||||||
|
) {
|
||||||
|
glyphs.push({
|
||||||
|
range: {
|
||||||
|
startLineNumber: parent.members[0].key[0].range.start.line + 1,
|
||||||
|
startColumn: 0,
|
||||||
|
endLineNumber:
|
||||||
|
parent.members[parent.members.length - 1].value[0].range.end
|
||||||
|
.line + 1,
|
||||||
|
endColumn: 10000,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
inlineClassName: 'disabled',
|
||||||
|
},
|
||||||
|
});
|
||||||
} else if (node.key[0].value === 'type') {
|
} else if (node.key[0].value === 'type') {
|
||||||
if (node.value[0].value === 'toggle') {
|
if (node.value[0].value === 'toggle') {
|
||||||
toggles.push([
|
toggles.push([
|
||||||
@@ -1041,15 +1087,15 @@ require(['vs/editor/editor.main'], async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recurse(node.value[0], [...path, node.key[0].value]);
|
recurse(node.value[0], [...path, node.key[0].value], node);
|
||||||
}
|
}
|
||||||
} else if (node.type === 'array') {
|
} else if (node.type === 'array') {
|
||||||
for (let i = 0; i < node.members.length; i++) {
|
for (let i = 0; i < node.members.length; i++) {
|
||||||
recurse(node.members[i], [...path, i]);
|
recurse(node.members[i], [...path, i], node);
|
||||||
}
|
}
|
||||||
} else if (node.type === 'object') {
|
} else if (node.type === 'object') {
|
||||||
for (let member of node.members) {
|
for (let member of node.members) {
|
||||||
recurse(member, path);
|
recurse(member, path, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,11 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/toastify-js": "^1.12.3",
|
"@types/toastify-js": "^1.12.4",
|
||||||
"@vscode/webview-ui-toolkit": "1.4.0",
|
"@vscode/webview-ui-toolkit": "1.4.0",
|
||||||
"json-ast-comments": "1.1.1",
|
"json-ast-comments": "1.1.1",
|
||||||
"monaco-editor": "0.45.0",
|
"monaco-editor": "0.52.2",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"serve": "^14.2.1"
|
"serve": "^14.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
25
web/pnpm-lock.yaml
generated
25
web/pnpm-lock.yaml
generated
@@ -6,8 +6,8 @@ settings:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/toastify-js':
|
'@types/toastify-js':
|
||||||
specifier: ^1.12.3
|
specifier: ^1.12.4
|
||||||
version: 1.12.3
|
version: 1.12.4
|
||||||
'@vscode/webview-ui-toolkit':
|
'@vscode/webview-ui-toolkit':
|
||||||
specifier: 1.4.0
|
specifier: 1.4.0
|
||||||
version: 1.4.0(react@18.2.0)
|
version: 1.4.0(react@18.2.0)
|
||||||
@@ -15,8 +15,11 @@ devDependencies:
|
|||||||
specifier: 1.1.1
|
specifier: 1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
monaco-editor:
|
monaco-editor:
|
||||||
specifier: 0.45.0
|
specifier: 0.52.2
|
||||||
version: 0.45.0
|
version: 0.52.2
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.6.2
|
||||||
|
version: 3.6.2
|
||||||
serve:
|
serve:
|
||||||
specifier: ^14.2.1
|
specifier: ^14.2.1
|
||||||
version: 14.2.1
|
version: 14.2.1
|
||||||
@@ -52,8 +55,8 @@ packages:
|
|||||||
exenv-es6: 1.1.1
|
exenv-es6: 1.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/toastify-js@1.12.3:
|
/@types/toastify-js@1.12.4:
|
||||||
resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==}
|
resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
|
/@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
|
||||||
@@ -415,8 +418,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/monaco-editor@0.45.0:
|
/monaco-editor@0.52.2:
|
||||||
resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==}
|
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ms@2.0.0:
|
/ms@2.0.0:
|
||||||
@@ -460,6 +463,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
|
resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/prettier@3.6.2:
|
||||||
|
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/punycode@1.4.1:
|
/punycode@1.4.1:
|
||||||
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
Reference in New Issue
Block a user