Compare commits

..

50 Commits

Author SHA1 Message Date
4432c7b9a0 Update WebhookManager.mc
Speculative fix to handle the callback data from webhook generation perhaps not being Lang.Dict.
2024-08-30 15:49:06 +01:00
15e2b19193 Deprecate template type (#177) 2024-08-30 14:32:22 +01:00
1b40231360 Fix errors 2024-08-30 13:49:09 +01:00
446c579660 Merge branch 'main' into 176-deprecate-template-items 2024-08-30 13:26:31 +01:00
da2e94caa3 Update TroubleShooting.md
Added instructions for how to send the debug log from the watch.
2024-08-30 13:25:41 +01:00
1c182dd615 Deprecate template type 2024-08-30 13:25:16 +01:00
fb5c2193f1 Create debug_log_location.png
Need a picture to explain to users where to fetch the debug logs from. Only gives the error message, but not the location unfortunately.
2024-08-30 13:18:09 +01:00
df3be94bf9 Documentation for v2.19 2024-08-29 23:12:13 +01:00
17162c14f2 174 tidy up two small issues (#175)
Heading & vibrate
2024-08-29 21:18:03 +01:00
b5140ce8b4 Added vibrate to confirm toggle items tapped 2024-08-29 21:13:55 +01:00
17a5d89998 Prevent negative heading values. 2024-08-29 21:00:50 +01:00
63bc127aec Perform all updates in one request (#173) 2024-08-29 20:55:40 +01:00
af43fde963 Merge branch 'main' into one-update-request 2024-08-29 19:03:12 +01:00
bbd9119a07 Update HomeAssistantToggleMenuItem.mc
Prevent multiple toggles for initiating one action. Only set the state when the called service returns with the confirmed value.
2024-08-28 23:01:28 +01:00
47a8a6e4e6 New poll delay property id 2024-08-28 08:52:41 +01:00
b476da6667 Revert compile_sim.cmd
Signed-off-by: Joseph Abbey <me@josephabbey.dev>
2024-08-28 08:45:07 +01:00
bd37d5f2a8 Allow toggles to work if template fails 2024-08-26 20:11:19 +01:00
2a48790f9c Individual errors 2024-08-26 20:09:24 +01:00
0feecde178 165 templating content for all types (#170)
Submitting as an initial draft in order to guage feedback on the style.
In particular the solution to adding the code to the `toggle` menu item,
which has incurred duplication of code as I can't inherit from two
classes.
2024-08-26 19:42:51 +01:00
e9a0c5d137 Single request to update 2024-08-26 18:59:17 +01:00
d387152593 Restyle 165 templating content for all types (#171)
Automated style fixes for #170, created by [Restyled][].

The following restylers [made
fixes](https://restyled.io/gh/house-of-abbey/repos/GarminHomeAssistant/jobs/4738467):

- [jq](https://stedolan.github.io/jq/)
- [prettier-json](https://prettier.io/docs/en/options.html#parser)
-
[whitespace](https://github.com/restyled-io/restyler-whitespace/blob/master/README.md)


To incorporate these changes, merge this Pull Request into the original.
We
recommend using the Squash or Rebase strategies.


**NOTE**: As work continues on the original Pull Request, this process
will
re-run and update (force-push) this Pull Request with updated style
fixes as
necessary. If the style is fixed manually at any point (i.e. this
process finds
no fixes to make), this Pull Request will be closed automatically.

Sorry if this was unexpected. To disable it, see our [documentation][].

[restyled]: https://restyled.io
[documentation]:
https://github.com/restyled-io/restyled.io/wiki/Disabling-Restyled
2024-08-25 19:47:20 +01:00
2ea349bfda Merge branch 'main' into 165-templating-content-for-all-types 2024-08-25 19:44:58 +01:00
a35798f9d3 Restyled by whitespace 2024-08-25 18:42:34 +00:00
4707f1ea9e Restyled by prettier-json 2024-08-25 18:42:33 +00:00
3424576027 Restyled by jq 2024-08-25 18:42:32 +00:00
1846d682f7 Update HISTORY.md
v2.18 text line amended.
2024-08-25 19:39:27 +01:00
685cda7924 Added template option to toggle menu items 2024-08-25 19:34:29 +01:00
72e825566c Update TemplateMenuItem.mc
Fixed System.println() calls.
2024-08-25 19:33:33 +01:00
1dc95eeac7 Added callback function to TemplateMenuItem 2024-08-25 18:53:32 +01:00
08829dac1f Update export.cmd
Trivial update to give the cmd.exe a title.
2024-08-25 16:50:08 +01:00
ca6e0733c5 Added null checking to activity stats (#169)
Position.getInfo().* might return null sometimes, so best check and
avoid a numerical error.

@KPWhiver My co-author is away presently, would you mind giving this
simple change the once over? Thanks!
2024-08-25 16:15:01 +01:00
01f073e67b Update BackgroundServiceDelegate.mc
Only submitting non-null values to HA, not fake zeros for null values.
2024-08-25 13:50:29 +01:00
64a9c5f274 Added null checking to activity stats
Position.getInfo().* might return null sometimes, so best check and avoid a numerical error.
2024-08-25 13:07:23 +01:00
ea32d71a2b Added templates to group items 2024-08-24 18:31:47 +01:00
f2fb7f65a0 Merge branch 'main' of https://github.com/house-of-abbey/GarminHomeAssistant 2024-08-24 14:30:44 +01:00
1e103069bc Update README.md
Feature differences between Application and Widget.
2024-08-24 14:30:23 +01:00
520309729d Bug fix for activity reporting (#167)
Added 'has' clauses around additional ActivityMonitor.Info fields that
are not present on all devices. I have also checked the other reporting
fields to ensure they are not device dependent.
2024-08-24 14:26:54 +01:00
d2aec16811 Update BackgroundServiceDelegate.mc
Failed to make commensurate changes to the background service code.
2024-08-24 14:21:02 +01:00
a424e35784 Update WebhookManager.mc
Returned internationalized string for unavailable to "unknown" as per review comments advice.
2024-08-24 13:58:47 +01:00
5558e25bda Bug fix for activity reporting
Added 'has' clauses around additional ActivityMonitor.Info fields that are not present on all devices.
2024-08-24 12:28:54 +01:00
769731bff2 Send heartrate, floor, and respiration rate values to Home Assistant (#162)
This change sends the heartrate, respiration rate, stepcount, and floors
climbed/descended to Home Assistant as mobile app entities. There are a
lot more sensors that could potentially be added.

I have only tested this on a Vivoactive 4s as that is the only device I
own.

Let me know what you think and if there are any changes you would like
to see.
2024-08-20 09:01:00 +01:00
2c56155593 Merge branch 'main' into more-mobile-app-sensors
Signed-off-by: Philip Abbey <philipabbey@users.noreply.github.com>
2024-08-19 12:36:33 +01:00
c38f91f456 Fixed merge conflict
Fixed a merge conflict made by more recent changes to fix a bug (https://github.com/house-of-abbey/GarminHomeAssistant/pull/164)
2024-08-19 12:33:23 +01:00
94d806c4d3 163 application crashes when started out of bluetooth range (#164)
Test the phone is connected before attempting WebHook HTTP requests.
2024-08-18 14:15:52 +01:00
51081ee2e6 Update HISTORY.md
Added v2.16
2024-08-18 13:03:30 +01:00
7c7130367f Update Settings.mc
Rearranged 'else' clauses.
2024-08-18 12:26:45 +01:00
42d1a7233c Update Settings.mc
Test the phone is connected before attempting WebHook HTTP requests.
2024-08-18 12:04:34 +01:00
43378bfe8c Set icons for sent sensor values 2024-08-05 19:34:26 +02:00
700e7ca822 Send heartrate, floor, and respiration rate values to Home Assistant 2024-08-05 18:02:37 +02:00
19642c6679 Update HomeAssistantTemplateMenuItem.mc (#158)
Template errors now displayed per item.
2024-07-26 18:37:27 +00:00
22 changed files with 674 additions and 592 deletions

View File

@ -24,7 +24,11 @@
| 2.9 | Added an option to enable confirmation vibration so it can be turned off by request of a user. Removed a redundant setting for the alternative Widget version that was not removed previously, and fixed a bug with dereferencing Null. |
| 2.10 | Added a user requested feature to slow down the rate of API calls in order to reduce battery wear for a situation where the application is kept open permanently on the device for convenience. Added 4 new devices. |
| 2.11 | Bug fix release for menu caching being turned off and language corrections (Czech & Slovenian). |
| 2.12 | Re-enabled Edge 540 and Edge 840 devices which we are unable to support due to simulator issues, but the Edge 840 device has been confirmed as working by a @Petucky. |
| 2.12 | Re-enabled Edge 540 and Edge 840 devices which we are unable to support due to simulator issues, but the Edge 840 device has been confirmed as working by a [Petucky](https://github.com/Petucky). |
| 2.13 | Moved the template status queries to Webhooks in order to fix the situation where an account is a non-privileged user. Added telemetry update on activity completion to make automations more timely at the end of an activity. When using a polling delay, there is no longer a startup delay for status updates and an action will trigger an immediate round of updates. |
| 2.14 | Cautionary bug fix for the background service code where refactorisation spoilt some API level guard clauses. |
| 2.15 | Better support for templates by isolating erroneous returns and marking the menu item. |
| 2.16 | Bug fix for lack of phone connection when starting the application. Includes new activity reporting features from [KPWhiver](https://github.com/KPWhiver) covering steps, heart rate, floors climbed and descended, and respiration rate. |
| 2.17 | Bug fix for reporting activity metrics that are not found on some devices. |
| 2.18 | Bug fix for reporting activity metrics that might be `null` sometimes. This is unsimulatable situation, so this version is a change based on an informed guess. |
| 2.19 | A template to evaluate is now optionally allowed on both `group` and `toggle` menu items. The template to evaluate is non-optional on a `template` menu item. All updates are performed in a single HTTP GET request for efficiency. Bug fix for negative heading values. Vibration now (optionally) confirms toggle menu items being tapped. |

View File

@ -25,6 +25,16 @@ As of version 2.0, there are now two installable versions. For older devices bef
| Application (original) | For newer devices that allow glance views in their applications, e.g. Venu 2, the GarminHomeAssistant application can be started either from a glance or from the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/61c91d28-ec5e-438d-9f83-39e9f45b199d) application page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the application. The application can be started two different ways, either from the glance in the carousel, or as an application from the list of applications & activities. With the latter, it is worth marking the application as a favourite.<br/><img src="images/Venu2_app_start.png" width="200" title="Venu 2" style="margin:5px"/><img src="images/Vivoactive3_app_start.jpg" width="200" title="Venu 2" style="margin:5px"/><br/>If you place the application on your list of favourites, and rearrange it to appear near the top, then the item is just one button press away from the watch face. This second picture here shows the application menu on a Vivoactive 3 watch.<br/><img src="images/Venu2_glance_start.png" width="200" title="Venu 2" style="margin:5px"/><br/>On newer watches, you can also start the application from the glance carousel. The glance view here typically displays some trackable status, so ours provides some early indication of availability. Older watches will still allow you to start this application from the list of applications and activities. |
| Widget | **"Maintenance only mode"** so no new features will be added to this version.<br>For older devices that use widgets, e.g. Venu (1) as opposed to applications with "glances", the GarminHomeAssistant application can instead be started from the widget carousel. This is a separate item in the Connect IQ AppStore and with this installation, the application will no longer appear in the list of applications and activities. Head over to the [GarminHomeAssistant](https://apps.garmin.com/en-US/apps/) widget page on the [Connect IQ application store](https://apps.garmin.com/en-US/) to download the widget.<br/><img src="images/Venu_Widget_sim.png" width="200" title="Venu 2" style="margin:5px"/><br/>Typically the widget view implements something similar to the glance view, e.g. status, and exists in a widget carousel to allow you to select an application to launch.<br>**Please note that memory in widgets is more limited than applications. This means a large menu definition can crash the widget without the code catching the error.**<br> This version was born out of the application version and from Ver 2.0 shared the same source code repository until Ver 2.8 when they were [separated](https://github.com/house-of-abbey/GarminHomeAssistantWidget) to allow the application version to take advantage of its increase memory availability. |
### Features
The following table lists the differences in functionality between the two. The Widget version is more limited due to memory constraints. As such new features are only being added to the Application.
| Feature | Application | Widget |
|---------|-------------|--------|
| Vibration | Optional setting | Always on |
| "Always on" support | Slow refresh option to reduce batter demand | No available |
| Metric reporting | Fuller, includes: activity, sub-activity, battery, charging, steps, heart rate, floors ascended and descended, respiration rate | Basic, includes: activity, sub-activity, battery only. |
### Source Code Repositories
* [Application](https://github.com/house-of-abbey/GarminHomeAssistant)

View File

@ -284,3 +284,29 @@ JSON for copy & paste:
]
}
```
# Debug Logs
As a desperate measure to assist with debugging the Home Assistant Application, you might be asked to send the authors a debug log.
![How to find the debug log file](images/debug_log_location.png)
The figure above shows how to find the file on Windows by attaching your watch by USB cable. Inside the `CIQ_LOG.YML` file there are often multiple entries, each looking like this:
```
Error: Unexpected Type Error
Details: 'Failed invoking <symbol>'
Time: 2024-08-30T12:00:25Z
Part-Number: 006-B3703-00
Firmware-Version: '19.05'
Language-Code: eng
ConnectIQ-Version: 4.2.4
Store-Id: 61c91d28-ec5e-438d-9f83-39e9f45b199d
Store-Version: 30
Filename: DCRL0437
Appname: HomeAssistant
Stack:
- pc: 0x10003b5e
```
The only useful information we can glean from this log is the first line, `Error: Unexpected Type Error`. There is no useful mapping to a line of code unless someone can explain to us how to use the `pc` line. Being able to send us the error type does serve as a clue.

View File

@ -22,14 +22,16 @@
"$ref": "#/$defs/entity"
},
"name": {
"title": "Your familiar name",
"type": "string"
"$ref": "#/$defs/name"
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"$ref": "#/$defs/type",
"const": "toggle"
},
"content": {
"$ref": "#/$defs/content",
"description": "Optional in a toggle."
},
"tap_action": {
"type": "object",
"properties": {
@ -55,17 +57,17 @@
"description": "Use 'tap_action' instead to mirror Home Assistant."
},
"name": {
"title": "Your familiar name",
"type": "string"
"$ref": "#/$defs/name"
},
"content": {
"title": "What to display (template)",
"type": "string"
"$ref": "#/$defs/content"
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"const": "template"
"$ref": "#/$defs/type",
"const": "template",
"deprecated": true,
"title": "Schema change:",
"description": "Use 'tap' instead."
}
},
"required": ["name", "content", "type"],
@ -78,17 +80,17 @@
"$ref": "#/$defs/entity"
},
"name": {
"title": "Your familiar name",
"type": "string"
"$ref": "#/$defs/name"
},
"content": {
"title": "What to display (template)",
"type": "string"
"$ref": "#/$defs/content"
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"const": "template"
"$ref": "#/$defs/type",
"const": "template",
"deprecated": true,
"title": "Schema change:",
"description": "Use 'tap' instead."
},
"tap_action": {
"$ref": "#/$defs/tap_action"
@ -106,14 +108,16 @@
"$ref": "#/$defs/entity"
},
"name": {
"title": "Your familiar name",
"type": "string"
"$ref": "#/$defs/name"
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"$ref": "#/$defs/type",
"const": "tap"
},
"content": {
"$ref": "#/$defs/content",
"description": "Optional in a tap."
},
"service": {
"$ref": "#/$defs/entity",
"deprecated": true,
@ -124,14 +128,7 @@
"$ref": "#/$defs/tap_action"
}
},
"oneOf": [
{
"required": ["name", "type", "service"]
},
{
"required": ["name", "type", "tap_action"]
}
],
"required": ["name", "type"],
"additionalProperties": false
},
"group": {
@ -153,10 +150,13 @@
"type": "string"
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'.",
"$ref": "#/$defs/type",
"const": "group"
},
"content": {
"$ref": "#/$defs/content",
"description": "Optional in a group."
},
"items": {
"$ref": "#/$defs/items"
}
@ -164,6 +164,10 @@
"required": ["name", "title", "type", "items"],
"additionalProperties": false
},
"type": {
"title": "Menu item type",
"description": "One of 'tap', 'template', 'toggle' or 'group'."
},
"items": {
"type": "array",
"maxItems": 16,
@ -184,6 +188,10 @@
]
}
},
"name": {
"title": "Your familiar name",
"type": "string"
},
"entity": {
"type": "string",
"title": "Home Assistant entity name",
@ -213,6 +221,10 @@
},
"required": ["service"]
},
"content": {
"title": "Jinja2 template defining the text to display.",
"type": "string"
},
"confirm": {
"type": "boolean",
"default": false,

View File

@ -2,7 +2,7 @@
# Actions
A simple example using a scene as a `tap`` menu item.
A simple example using a scene as a `tap` menu item.
```json
{

View File

@ -43,6 +43,17 @@ Then you can use the following in your config:
}
```
And you can optionally include a template to reflect some status. See [Templates](Templates.md) for details on hwo to use this JSON field.
```json
{
"entity": "switch.<switch-name>",
"name": "<name>",
"type": "toggle",
"content": "..."
}
```
## Example - Covers
```yaml

View File

@ -10,6 +10,9 @@ In order to provide the most functionality possible the content of the menu item
- `{{` ... `}}` for Expressions to print to the template output
- `{#` ... `#}` for Comments not included in the template output
> [!IMPORTANT]
> In order to avoid "Template Error" being displayed as the return value, make sure your Jinja2 template returns a `string`, not a number of some variety. _All numbers must be formatted to strings_ so the application does not need to distinguish an `integer` from a `float`.
## States
In this example we get the battery level of the device and add the percent sign. *Very simple*
@ -115,6 +118,20 @@ Note: Only when you use the `tap_action` field do you also need to include the `
}
```
## Group and Toggle Menu Items
Both `group` and `toggle` menu items accept an optional `content` field as of v2.19. This allows the use of templates to present status information.
```json
{
"name": "Each Lounge Light",
"title": "Lounge",
"type": "group",
"content": "{{'On: %d, Off: %d'|format(expand(state_attr('light.living_room_lights', 'entity_id'))|selectattr('state','eq','on')|map(attribute='entity_id')|list|count, expand(state_attr('light.living_room_lights', 'entity_id'))|selectattr('state','eq','off')|map(attribute='entity_id')|list|count)}}",
"items": [..]
}
```
## Advanced
Here we generate a bar graph of the battery level. We use the following steps to do this:

View File

@ -69,6 +69,8 @@ rem -x,--excludes <arg> Add annotations to the exclude list (deprecated)
rem -y,--private-key <arg> Private key to sign builds with
rem -z,--rez <arg> Resource files (deprecated)
title Exporting Garmin Home Assistant Application
rem Batch file's directory where the source code is
set SRC=%~dp0
rem drop last character '\'

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -56,7 +56,7 @@
this delayfor an "always open" mode of operation, which then drains the
watch battery from the additional API access activity.
-->
<property id="poll_delay" type="number">0</property>
<property id="poll_delay_combined" type="number">5</property>
<!--
After this time (in seconds), a confirmation dialog for an action is

View File

@ -66,7 +66,7 @@
</setting>
<setting
propertyKey="@Properties.poll_delay"
propertyKey="@Properties.poll_delay_combined"
title="@Strings.SettingsPollDelay"
>
<settingConfig type="numeric" min="0" />

View File

@ -79,13 +79,13 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) {
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
var position = Position.getInfo();
// System.println("BackgroundServiceDelegate onTemporalEvent(): gps: " + position.position.toDegrees());
// System.println("BackgroundServiceDelegate onTemporalEvent(): speed: " + position.speed);
// System.println("BackgroundServiceDelegate onTemporalEvent(): course: " + position.heading + "rad (" + (position.heading * 180 / Math.PI) + "°)");
// System.println("BackgroundServiceDelegate onTemporalEvent(): altitude: " + position.altitude);
// System.println("BackgroundServiceDelegate onTemporalEvent(): battery: " + System.getSystemStats().battery);
// System.println("BackgroundServiceDelegate onTemporalEvent(): charging: " + System.getSystemStats().charging);
// System.println("BackgroundServiceDelegate onTemporalEvent(): activity: " + Activity.getProfileInfo().name);
// System.println("BackgroundServiceDelegate onTemporalEvent(): GPS : " + position.position.toDegrees());
// System.println("BackgroundServiceDelegate onTemporalEvent(): Speed : " + position.speed);
// System.println("BackgroundServiceDelegate onTemporalEvent(): Course : " + position.heading + " radians (" + (position.heading * 180 / Math.PI) + "°)");
// System.println("BackgroundServiceDelegate onTemporalEvent(): Altitude : " + position.altitude);
// System.println("BackgroundServiceDelegate onTemporalEvent(): Battery : " + System.getSystemStats().battery);
// System.println("BackgroundServiceDelegate onTemporalEvent(): Charging : " + System.getSystemStats().charging);
// System.println("BackgroundServiceDelegate onTemporalEvent(): Activity : " + Activity.getProfileInfo().name);
// Don't use Settings.* here as the object lasts < 30 secs and is recreated each time the background service is run
@ -102,17 +102,32 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
accuracy = 10;
break;
}
var data = { "gps_accuracy" => accuracy };
// Only add the non-null fields as all the values are optional in Home Assistant, and it avoid submitting fake values.
if (position.position != null) {
data.put("gps", position.position.toDegrees());
}
if (position.speed != null) {
data.put("speed", Math.round(position.speed));
}
if (position.heading != null) {
var heading = Math.round(position.heading * 180 / Math.PI);
while (heading < 0) {
heading += 360;
}
data.put("course", heading);
}
if (position.altitude != null) {
data.put("altitude", Math.round(position.altitude));
}
// System.println("BackgroundServiceDelegate onTemporalEvent(): data = " + data.toString());
Communications.makeWebRequest(
(Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String),
{
"type" => "update_location",
"data" => {
"gps" => position.position.toDegrees(),
"gps_accuracy" => accuracy,
"speed" => Math.round(position.speed),
"course" => Math.round(position.heading * 180 / Math.PI),
"altitude" => Math.round(position.altitude),
}
"data" => data,
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
@ -124,18 +139,62 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
method(:onReturnBatteryUpdate)
);
}
var activityInfo = ActivityMonitor.getInfo();
var heartRate = Activity.getActivityInfo().currentHeartRate;
var data = [
{
"state" => System.getSystemStats().battery,
"type" => "sensor",
"unique_id" => "battery_level"
"unique_id" => "battery_level",
"icon" => "mdi:battery"
},
{
"state" => System.getSystemStats().charging,
"type" => "binary_sensor",
"unique_id" => "battery_is_charging"
"unique_id" => "battery_is_charging",
"icon" => System.getSystemStats().charging ? "mdi:battery-plus" : "mdi:battery-minus"
},
{
"state" => activityInfo.steps == null ? "unknown" : activityInfo.steps,
"type" => "sensor",
"unique_id" => "steps_today",
"icon" => "mdi:walk"
},
{
"state" => heartRate == null ? "unknown" : heartRate,
"type" => "sensor",
"unique_id" => "heart_rate",
"icon" => "mdi:heart-pulse"
}
];
if (ActivityMonitor.Info has :floorsClimbed) {
data.add({
"state" => activityInfo.floorsClimbed == null ? "unknown" : activityInfo.floorsClimbed,
"type" => "sensor",
"unique_id" => "floors_climbed_today",
"icon" => "mdi:stairs-up"
});
}
if (ActivityMonitor.Info has :floorsDescended) {
data.add({
"state" => activityInfo.floorsDescended == null ? "unknown" : activityInfo.floorsDescended,
"type" => "sensor",
"unique_id" => "floors_descended_today",
"icon" => "mdi:stairs-down"
});
}
if (ActivityMonitor.Info has :respirationRate) {
data.add({
"state" => activityInfo.respirationRate == null ? "unknown" : activityInfo.respirationRate,
"type" => "sensor",
"unique_id" => "respiration_rate",
"icon" => "mdi:lungs"
});
}
if (activity != null) {
data.add({
"state" => activity,

View File

@ -116,10 +116,10 @@ class ErrorView extends ScalableView {
static function unShow() as Void {
if (mShown) {
WatchUi.popView(WatchUi.SLIDE_DOWN);
// The call to 'updateNextMenuItem()' must be on another thread so that the view is popped above.
// The call to 'updateMenuItems()' must be on another thread so that the view is popped above.
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiResume, false);
myTimer.start(getApp().method(:updateMenuItems), Globals.scApiResume, false);
// This must be last to avoid a race condition with show(), where the
// ErrorView can't be dismissed.
mShown = false;

View File

@ -34,11 +34,9 @@ class HomeAssistantApp extends Application.AppBase {
private var mGlanceTimer as Timer.Timer or Null;
private var mUpdateTimer as Timer.Timer or Null;
// Array initialised by onReturnFetchMenuConfig()
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> or Null;
private var mNextItemToUpdate as Lang.Number = 0; // Index into the above array
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> or Null;
private var mIsGlance as Lang.Boolean = false;
private var mIsApp as Lang.Boolean = false; // Or Widget
private var mIsInitUpdateCompl as Lang.Boolean = false;
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
function initialize() {
@ -262,15 +260,129 @@ class HomeAssistantApp extends Application.AppBase {
mQuitTimer.begin();
}
var mTemplates as Lang.Dictionary = {};
function startUpdates() {
if (mHaMenu != null and !mUpdating) {
mItemsToUpdate = mHaMenu.getItemsToUpdate();
// Start the continuous update process that continues for as long as the application is running.
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
if (mItemsToUpdate.size() > 0) {
mUpdating = true;
updateNextMenuItemInternal();
mTemplates = {};
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var template = item.buildTemplate();
if (template != null) {
mTemplates.put(i.toString(), {
"template" => template
});
}
if (item instanceof HomeAssistantToggleMenuItem) {
mTemplates.put(i.toString() + "t", {
"template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate()
});
}
}
updateMenuItems();
}
}
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(method(:updateMenuItems), Globals.scApiBackoff, false);
// Revert status
status = getApiStatus();
break;
case 404:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
case 400:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 400, bad request. Template error.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var state = data.get(i.toString());
item.updateState(state);
if (item instanceof HomeAssistantToggleMenuItem) {
(item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t"));
}
}
var delay = Settings.getPollDelay();
if (delay > 0) {
mUpdateTimer.start(method(:updateMenuItems), delay, false);
} else {
updateMenuItems();
}
break;
default:
// System.println("HomeAssistantApp onReturnUpdateMenuItems(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
setApiStatus(status);
}
function updateMenuItems() as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else {
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
url,
{
"type" => "render_template",
"data" => mTemplates
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnUpdateMenuItems)
);
}
}
@ -403,46 +515,14 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
}
function updateNextMenuItem() as Void {
var delay = Settings.getPollDelay();
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
} else {
updateNextMenuItemInternal();
}
}
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take
// alternative action if the test fails.
function forceStatusUpdates() as Void {
// Don't mess with updates unless we are using a timer.
if (Settings.getPollDelay() > 0) {
mUpdateTimer.stop();
mIsInitUpdateCompl = false;
// Start from the beginning, or we will only get a partial round of updates before mIsInitUpdateCompl is flipped.
mNextItemToUpdate = 0;
// For immediate updates
updateNextMenuItem();
}
}
// We need to spread out the API calls so as not to overload the results queue and cause Communications.BLE_QUEUE_FULL
// (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms.
function updateNextMenuItemInternal() as Void {
var itu = mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem>;
if (itu != null) {
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
itu[mNextItemToUpdate].getState();
// mNextItemToUpdate = (mNextItemToUpdate + 1) % itu.size() - But with roll-over detection
if (mNextItemToUpdate == itu.size()-1) {
// Last item completed return to the start of the list
mNextItemToUpdate = 0;
mIsInitUpdateCompl = true;
} else {
mNextItemToUpdate++;
}
// } else {
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
updateMenuItems();
}
}

View File

@ -14,7 +14,8 @@
//
// Description:
//
// Menu button with an icon that opens a sub-menu, i.e. group.
// Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
// a Home Assistant Template.
//
//-----------------------------------------------------------------------------------
@ -22,14 +23,17 @@ using Toybox.Lang;
using Toybox.WatchUi;
class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String or Null;
private var mMenu as HomeAssistantView;
function initialize(
definition as Lang.Dictionary,
template as Lang.String,
icon as WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment
} or Null) {
} or Null
) {
WatchUi.IconMenuItem.initialize(
definition.get("name") as Lang.String,
@ -39,11 +43,39 @@ class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
options
);
mTemplate = template;
mMenu = new HomeAssistantView(definition, null);
}
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel(null);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantGroupMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function getMenuView() as HomeAssistantView {
return mMenu;
}
function hasTemplate() as Lang.Boolean {
return mTemplate != null;
}
}

View File

@ -67,17 +67,19 @@ class HomeAssistantMenuItemFactory {
function toggle(
label as Lang.String or Lang.Symbol,
entity_id as Lang.String or Null,
template as Lang.String or Null,
confirm as Lang.Boolean
) as WatchUi.MenuItem {
return new HomeAssistantToggleMenuItem(
label,
template,
confirm,
{ "entity_id" => entity_id },
mMenuItemOptions
);
}
function template_tap(
function tap(
label as Lang.String or Lang.Symbol,
entity as Lang.String or Null,
template as Lang.String or Null,
@ -92,60 +94,40 @@ class HomeAssistantMenuItemFactory {
data.put("entity_id", entity);
}
}
return new HomeAssistantTemplateMenuItem(
label,
template,
service,
confirm,
data,
mTapTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
}
function template_notap(
label as Lang.String or Lang.Symbol,
template as Lang.String or Null
) as WatchUi.MenuItem {
return new HomeAssistantTemplateMenuItem(
label,
template,
null,
false,
null,
mInfoTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
}
function tap(
label as Lang.String or Lang.Symbol,
entity as Lang.String or Null,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null
) as WatchUi.MenuItem {
if (entity != null) {
if (data == null) {
data = { "entity_id" => entity };
} else {
data.put("entity_id", entity);
}
if (service != null) {
return new HomeAssistantTapMenuItem(
label,
template,
service,
confirm,
data,
mTapTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
} else {
return new HomeAssistantTapMenuItem(
label,
template,
service,
confirm,
data,
mInfoTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
}
return new HomeAssistantTapMenuItem(
label,
service,
confirm,
data,
mTapTypeIcon,
mMenuItemOptions,
mHomeAssistantService
);
}
function group(definition as Lang.Dictionary) as WatchUi.MenuItem {
return new HomeAssistantGroupMenuItem(definition, mGroupTypeIcon, mMenuItemOptions);
function group(
definition as Lang.Dictionary,
template as Lang.String or Null
) as WatchUi.MenuItem {
return new HomeAssistantGroupMenuItem(
definition,
template,
mGroupTypeIcon,
mMenuItemOptions
);
}
}

View File

@ -24,20 +24,22 @@ using Toybox.Graphics;
class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String;
private var mTemplate as Lang.String;
private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
function initialize(
label as Lang.String or Lang.Symbol,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
label as Lang.String or Lang.Symbol,
template as Lang.String,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment
} or Null,
haService as HomeAssistantService
haService as HomeAssistantService
) {
WatchUi.IconMenuItem.initialize(
label,
@ -48,11 +50,39 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
);
mHomeAssistantService = haService;
mTemplate = template;
mService = service;
mConfirm = confirm;
mData = data;
}
function hasTemplate() as Lang.Boolean {
return mTemplate != null;
}
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function callService() as Void {
if (mConfirm) {
WatchUi.pushView(
@ -61,13 +91,15 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
WatchUi.SLIDE_IMMEDIATE
);
} else {
mHomeAssistantService.call(mService, mData);
onConfirm(false);
}
}
// NB. Parameter 'b' is ignored
function onConfirm(b as Lang.Boolean) as Void {
mHomeAssistantService.call(mService, mData);
if (mService != null) {
mHomeAssistantService.call(mService, mData);
}
}
}

View File

@ -1,194 +0,0 @@
//-----------------------------------------------------------------------------------
//
// Distributed under MIT Licence
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
//
//-----------------------------------------------------------------------------------
//
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey, 12 January 2024
//
//
// Description:
//
// Menu button that renders a Home Assistant Template, and optionally triggers a service.
//
// Reference:
// * https://developers.home-assistant.io/docs/api/rest/
// * https://www.home-assistant.io/docs/configuration/templating
//
//-----------------------------------------------------------------------------------
using Toybox.Lang;
using Toybox.WatchUi;
using Toybox.Graphics;
class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
private var mHomeAssistantService as HomeAssistantService;
private var mTemplate as Lang.String;
private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary or Null;
function initialize(
label as Lang.String or Lang.Symbol,
template as Lang.String,
service as Lang.String or Null,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
icon as Graphics.BitmapType or WatchUi.Drawable,
options as {
:alignment as WatchUi.MenuItem.Alignment
} or Null,
haService as HomeAssistantService
) {
WatchUi.IconMenuItem.initialize(
label,
null,
null,
icon,
options
);
mHomeAssistantService = haService;
mTemplate = template;
mService = service;
mConfirm = confirm;
mData = data;
}
function callService() as Void {
if (mConfirm) {
WatchUi.pushView(
new HomeAssistantConfirmation(),
new HomeAssistantConfirmationDelegate(method(:onConfirm), false),
WatchUi.SLIDE_IMMEDIATE
);
} else {
onConfirm(false);
}
}
// NB. Parameter 'b' is ignored
function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) {
mHomeAssistantService.call(mService, mData);
}
}
// Callback function after completing the GET request to fetch the status.
// Terminate updating the toggle menu items via the chain of calls for a permanent network
// error. The ErrorView cancellation will resume the call chain.
//
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break;
case 404:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
case 400:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 400, bad request. Template error.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
var label = data.get("request");
if (label == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(label instanceof Lang.String) {
setSubLabel(label);
} else if(label instanceof Lang.Dictionary) {
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() label = " + label);
if (label.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
}
requestUpdate();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
break;
default:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
getApp().setApiStatus(status);
}
function getState() as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else {
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
url,
{
"type" => "render_template",
"data" => {
"request" => {
"template" => mTemplate
}
}
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnGetState)
);
}
}
}

View File

@ -25,140 +25,89 @@ using Toybox.Application.Properties;
using Toybox.Timer;
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary;
private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary;
private var mTemplate as Lang.String;
private var mHasVibrate as Lang.Boolean = false;
function initialize(
label as Lang.String or Lang.Symbol,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
options as {
label as Lang.String or Lang.Symbol,
template as Lang.String,
confirm as Lang.Boolean,
data as Lang.Dictionary or Null,
options as {
:alignment as WatchUi.MenuItem.Alignment,
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol
} or Null
) {
WatchUi.ToggleMenuItem.initialize(label, null, null, false, options);
mConfirm = confirm;
mData = data;
if (Attention has :vibrate) {
mHasVibrate = true;
}
mConfirm = confirm;
mData = data;
mTemplate = template;
}
private function setUiToggle(state as Null or Lang.String) as Void {
if (state != null) {
if (state.equals("on") && !isEnabled()) {
setEnabled(true);
WatchUi.requestUpdate();
} else if (state.equals("off") && isEnabled()) {
setEnabled(false);
WatchUi.requestUpdate();
}
}
}
// Callback function after completing the GET request to fetch the status.
// Terminate updating the toggle menu items via the chain of calls for a permanent network
// error. The ErrorView cancellation will resume the call chain.
//
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode);
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break;
case 404:
var msg = null;
if (data != null) {
msg = data.get("message");
}
if (msg != null) {
// Should be an HTTP 404 according to curl queries
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404. " + mData.get("entity_id") + " " + msg);
ErrorView.show("HTTP 404, " + mData.get("entity_id") + ". " + data.get("message"));
} else {
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
}
break;
case 405:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 405. " + mData.get("entity_id") + " " + data.get("message"));
ErrorView.show("HTTP 405, " + mData.get("entity_id") + ". " + data.get("message"));
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
var state = data.get("state") as Lang.String;
// System.println((data.get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
if (getLabel().equals("...")) {
setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String);
}
setUiToggle(state);
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
break;
default:
// System.println("HomeAssistantToggleMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
getApp().setApiStatus(status);
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function buildToggleTemplate() as Lang.String or Null {
return "{{states('" + mData.get("entity_id") + "')}}";
}
function getState() as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel(null);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
var url = Settings.getApiUrl() + "/states/" + mData.get("entity_id");
// System.println("HomeAssistantToggleMenuItem getState() URL=" + url);
Communications.makeWebRequest(
url,
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:headers => {
"Authorization" => "Bearer " + Settings.getApiKey()
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnGetState)
);
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setUiToggle("off");
} else if(data instanceof Lang.String) {
setUiToggle(data);
if (mTemplate == null and data.equals("unavailable")) {
setSubLabel($.Rez.Strings.Unavailable);
}
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
if (mTemplate == null) {
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
if (mTemplate == null) {
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
}
WatchUi.requestUpdate();
}
// Callback function after completing the POST request to set the status.
@ -205,6 +154,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
state = d[i].get("state") as Lang.String;
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
setUiToggle(state);
WatchUi.requestUpdate();
}
}
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
@ -218,15 +168,14 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
}
function setState(s as Lang.Boolean) as Void {
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
setEnabled(!isEnabled());
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
// Toggle the UI back
setEnabled(!isEnabled());
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
// Toggle the UI back
setEnabled(!isEnabled());
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
} else {
// Updated SDK and got a new error
@ -253,6 +202,13 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
},
method(:onReturnSetState)
);
if (mHasVibrate and Settings.getVibrate()) {
Attention.vibrate([
new Attention.VibeProfile(50, 100), // On for 100ms
new Attention.VibeProfile( 0, 100), // Off for 100ms
new Attention.VibeProfile(50, 100) // On for 100ms
]);
}
}
}

View File

@ -62,36 +62,37 @@ class HomeAssistantView extends WatchUi.Menu2 {
}
if (type != null && name != null) {
if (type.equals("toggle") && entity != null) {
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, confirm));
} else if (type.equals("template") && content != null) {
if (service == null) {
addItem(HomeAssistantMenuItemFactory.create().template_notap(name, content));
} else {
addItem(HomeAssistantMenuItemFactory.create().template_tap(name, entity, content, service, confirm, data));
}
} else if (type.equals("tap") && service != null) {
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, service, confirm, data));
addItem(HomeAssistantMenuItemFactory.create().toggle(name, entity, content, confirm));
} else if ((type.equals("tap") && service != null) || (type.equals("template") && content != null)) {
addItem(HomeAssistantMenuItemFactory.create().tap(name, entity, content, service, confirm, data));
} else if (type.equals("group")) {
addItem(HomeAssistantMenuItemFactory.create().group(items[i]));
addItem(HomeAssistantMenuItemFactory.create().group(items[i], content));
}
}
}
}
}
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> {
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem> {
var fullList = [];
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
for(var i = 0; i < mItems.size(); i++) {
var item = lmi[i];
if (item instanceof HomeAssistantGroupMenuItem) {
// Group menu items can now have an optional template to evaluate
var gmi = item as HomeAssistantGroupMenuItem;
if (gmi.hasTemplate()) {
fullList.add(item);
}
fullList.addAll(item.getMenuView().getItemsToUpdate());
} else if (item instanceof HomeAssistantToggleMenuItem) {
fullList.add(item);
} else if (item instanceof HomeAssistantTemplateMenuItem) {
fullList.add(item);
} else if (item instanceof HomeAssistantTapMenuItem) {
var tmi = item as HomeAssistantTapMenuItem;
if (tmi.hasTemplate()) {
fullList.add(item);
}
}
}
@ -151,10 +152,6 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
var haItem = item as HomeAssistantTapMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId());
haItem.callService();
} else if (item instanceof HomeAssistantTemplateMenuItem) {
var haItem = item as HomeAssistantTemplateMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId());
haItem.callService();
} else if (item instanceof HomeAssistantGroupMenuItem) {
var haMenuItem = item as HomeAssistantGroupMenuItem;
// System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId());

View File

@ -57,7 +57,7 @@ class Settings {
mClearCache = Properties.getValue("clear_cache");
mVibrate = Properties.getValue("enable_vibration");
mAppTimeout = Properties.getValue("app_timeout");
mPollDelay = Properties.getValue("poll_delay");
mPollDelay = Properties.getValue("poll_delay_combined");
mConfirmTimeout = Properties.getValue("confirm_timeout");
mMenuAlignment = Properties.getValue("menu_alignment");
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
@ -70,36 +70,29 @@ class Settings {
// Manage this inside the application or widget only (not a glance or background service process)
if (mIsApp) {
if (mHasService) {
mWebhookManager = new WebhookManager();
if (getWebhookId().equals("")) {
// System.println("Settings update(): Doing full webhook & sensor creation.");
mWebhookManager.requestWebhookId();
} else {
// System.println("Settings update(): Doing just sensor creation.");
// We already have a Webhook ID, so just enable or disable the sensor in Home Assistant.
// Its a multiple step process, hence starting at step 0.
mWebhookManager.registerWebhookSensor({
"device_class" => "battery",
"name" => "Battery Level",
"state" => System.getSystemStats().battery,
"type" => "sensor",
"unique_id" => "battery_level",
"unit_of_measurement" => "%",
"state_class" => "measurement",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 0);
}
if (mIsSensorsLevelEnabled) {
// Create the timed activity
if ((Background.getTemporalEventRegisteredTime() == null) or
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
Background.registerForActivityCompletedEvent();
if (System.getDeviceSettings().phoneConnected) {
mWebhookManager = new WebhookManager();
if (getWebhookId().equals("")) {
// System.println("Settings update(): Doing full webhook & sensor creation.");
mWebhookManager.requestWebhookId();
} else {
// System.println("Settings update(): Doing just sensor creation.");
// We already have a Webhook ID, so just enable or disable the sensor in Home Assistant.
mWebhookManager.registerWebhookSensors();
}
} else if (Background.getTemporalEventRegisteredTime() != null) {
Background.deleteTemporalEvent();
Background.deleteActivityCompletedEvent();
if (mIsSensorsLevelEnabled) {
// Create the timed activity
if ((Background.getTemporalEventRegisteredTime() == null) or
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
Background.registerForActivityCompletedEvent();
}
} else if (Background.getTemporalEventRegisteredTime() != null) {
Background.deleteTemporalEvent();
Background.deleteActivityCompletedEvent();
}
} else {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
}
} else {
// Explicitly disable the background event which persists when the application closes.

View File

@ -69,17 +69,7 @@ class WebhookManager {
if (id != null) {
Settings.setWebhookId(id);
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering first sensor: Battery Level");
registerWebhookSensor({
"device_class" => "battery",
"name" => "Battery Level",
"state" => System.getSystemStats().battery,
"type" => "sensor",
"unique_id" => "battery_level",
"unit_of_measurement" => "%",
"state_class" => "measurement",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 0);
registerWebhookSensors();
} else {
// System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
Settings.unsetIsSensorsLevelEnabled();
@ -125,7 +115,7 @@ class WebhookManager {
);
}
function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, step as Lang.Number) as Void {
function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, sensors as Lang.Array<Lang.Object>) as Void {
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
@ -170,71 +160,28 @@ class WebhookManager {
case 200:
case 201:
var d = data as Lang.Dictionary;
if ((d.get("success") as Lang.Boolean or Null) != false) {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Success");
switch (step) {
case 0:
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Battery is Charging");
registerWebhookSensor({
"device_class" => "battery_charging",
"name" => "Battery is Charging",
"state" => System.getSystemStats().charging,
"type" => "binary_sensor",
"unique_id" => "battery_is_charging",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 1);
break;
case 1:
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity");
if (Activity has :getProfileInfo) {
var activity = Activity.getProfileInfo().sport;
if ((Activity.getActivityInfo() != null) and
((Activity.getActivityInfo().elapsedTime == null) or
(Activity.getActivityInfo().elapsedTime == 0))) {
// Indicate no activity with -1, not part of Garmin's activity codes.
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
activity = -1;
}
registerWebhookSensor({
"name" => "Activity",
"state" => activity,
"type" => "sensor",
"unique_id" => "activity",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 2);
break;
}
case 2:
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Sub-Activity");
if (Activity has :getProfileInfo) {
var sub_activity = Activity.getProfileInfo().subSport;
if ((Activity.getActivityInfo() != null) and
((Activity.getActivityInfo().elapsedTime == null) or
(Activity.getActivityInfo().elapsedTime == 0))) {
// Indicate no activity with -1, not part of Garmin's activity codes.
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
sub_activity = -1;
}
registerWebhookSensor({
"name" => "Sub-activity",
"state" => sub_activity,
"type" => "sensor",
"unique_id" => "sub_activity",
"disabled" => !Settings.isSensorsLevelEnabled()
}, 3);
break;
}
case 3:
if (data instanceof Lang.Dictionary) {
var d = data as Lang.Dictionary;
var b = d.get("success") as Lang.Boolean or Null;
if (b != null and b != false) {
if (sensors.size() == 0) {
getApp().startUpdates();
default:
} else {
registerWebhookSensor(sensors);
}
} else {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, no 'success'.");
Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
}
} else {
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
// !! Speculative code for an application crash !!
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, not a Lang.Dict");
// Webhook ID might have been deleted on Home Assistant server and a Lang.String is trying to tell us an error message
Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + data.toString());
}
break;
@ -242,11 +189,11 @@ class WebhookManager {
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
Settings.unsetWebhookId();
Settings.unsetIsSensorsLevelEnabled();
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + " " + responseCode);
}
}
function registerWebhookSensor(sensor as Lang.Object, step as Lang.Number) {
function registerWebhookSensor(sensors as Lang.Array<Lang.Object>) {
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
// System.println("WebhookManager registerWebhookSensor(): URL=" + url);
@ -255,7 +202,7 @@ class WebhookManager {
url,
{
"type" => "register_sensor",
"data" => sensor
"data" => sensors[0]
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
@ -263,10 +210,126 @@ class WebhookManager {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
:context => step
:context => sensors.slice(1, null)
},
method(:onReturnRegisterWebhookSensor)
);
}
function registerWebhookSensors() {
var activityInfo = ActivityMonitor.getInfo();
var heartRate = Activity.getActivityInfo().currentHeartRate;
var sensors = [
{
"device_class" => "battery",
"name" => "Battery Level",
"state" => System.getSystemStats().battery,
"type" => "sensor",
"unique_id" => "battery_level",
"icon" => "mdi:battery",
"unit_of_measurement" => "%",
"state_class" => "measurement",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
},
{
"device_class" => "battery_charging",
"name" => "Battery is Charging",
"state" => System.getSystemStats().charging,
"type" => "binary_sensor",
"unique_id" => "battery_is_charging",
"icon" => System.getSystemStats().charging ? "mdi:battery-plus" : "mdi:battery-minus",
"entity_category" => "diagnostic",
"disabled" => !Settings.isSensorsLevelEnabled()
},
{
"name" => "Steps today",
"state" => activityInfo.steps == null ? "unknown" : activityInfo.steps,
"type" => "sensor",
"unique_id" => "steps_today",
"icon" => "mdi:walk",
"state_class" => "total",
"disabled" => !Settings.isSensorsLevelEnabled()
},
{
"name" => "Heart rate",
"state" => heartRate == null ? "unknown" : heartRate,
"type" => "sensor",
"unique_id" => "heart_rate",
"icon" => "mdi:heart-pulse",
"unit_of_measurement" => "bpm",
"state_class" => "measurement",
"disabled" => !Settings.isSensorsLevelEnabled()
}
];
if (ActivityMonitor.Info has :floorsClimbed) {
sensors.add({
"name" => "Floors climbed today",
"state" => activityInfo.floorsClimbed == null ? "unknown" : activityInfo.floorsClimbed,
"type" => "sensor",
"unique_id" => "floors_climbed_today",
"icon" => "mdi:stairs-up",
"state_class" => "total",
"disabled" => !Settings.isSensorsLevelEnabled()
});
}
if (ActivityMonitor.Info has :floorsDescended) {
sensors.add({
"name" => "Floors descended today",
"state" => activityInfo.floorsDescended == null ? "unknown" : activityInfo.floorsDescended,
"type" => "sensor",
"unique_id" => "floors_descended_today",
"icon" => "mdi:stairs-down",
"state_class" => "total",
"disabled" => !Settings.isSensorsLevelEnabled()
});
}
if (ActivityMonitor.Info has :respirationRate) {
sensors.add({
"name" => "Respiration rate",
"state" => activityInfo.respirationRate == null ? "unknown" : activityInfo.respirationRate,
"type" => "sensor",
"unique_id" => "respiration_rate",
"icon" => "mdi:lungs",
"unit_of_measurement" => "bpm",
"state_class" => "measurement",
"disabled" => !Settings.isSensorsLevelEnabled()
});
}
if (Activity has :getProfileInfo) {
var activity = Activity.getProfileInfo().sport;
var sub_activity = Activity.getProfileInfo().subSport;
if ((Activity.getActivityInfo() != null) and
((Activity.getActivityInfo().elapsedTime == null) or
(Activity.getActivityInfo().elapsedTime == 0))) {
// Indicate no activity with -1, not part of Garmin's activity codes.
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
activity = -1;
sub_activity = -1;
}
sensors.add({
"name" => "Activity",
"state" => activity,
"type" => "sensor",
"unique_id" => "activity",
"disabled" => !Settings.isSensorsLevelEnabled()
});
sensors.add({
"name" => "Sub-activity",
"state" => sub_activity,
"type" => "sensor",
"unique_id" => "sub_activity",
"disabled" => !Settings.isSensorsLevelEnabled()
});
}
registerWebhookSensor(sensors);
}
}