mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-06-19 21:08:37 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
9030a00d7d | |||
7bd5e98a1a | |||
a49dd6554f | |||
d2fc64836a | |||
fc1a0eeb6d | |||
61f6d69e64 | |||
7f31cecfb5 | |||
98af5578f0 | |||
c18736e40d | |||
822c6ccca6 | |||
a139742265 | |||
dfa4cdd9b8 | |||
1c4add693d |
@ -25,3 +25,4 @@
|
|||||||
| 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.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.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. |
|
||||||
|
| 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. |
|
||||||
|
@ -43,7 +43,10 @@ Setup for this menu is more complicated than the Connect IQ settings menu really
|
|||||||
2. the file is editable within Home Assistant via "[Studio Code Server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_vscode)", and
|
2. the file is editable within Home Assistant via "[Studio Code Server](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a0d7b954_vscode)", and
|
||||||
3. the schema is verifiable using [JSON Schema](https://json-schema.org/overview/what-is-jsonschema).
|
3. the schema is verifiable using [JSON Schema](https://json-schema.org/overview/what-is-jsonschema).
|
||||||
|
|
||||||
We have used `/config/www/garmin/<something>.json` on our Home Assistant's file system. That equates to a URL of `https://homeassistant.local/local/garmin/<something>.json`.
|
We have used `/config/www/garmin/<something>.json` on our home brew Home Assistant's file system. That equates to a URL of `https://homeassistant.local/local/garmin/<something>.json`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> However [recent reports](https://community.home-assistant.io/t/www-folder-location-for-local-documents/24903/16) suggest this path may no longer work on [Nabu Casa](https://www.nabucasa.com/) and you should use `/homeassistant/www/` instead of `/config/www/`. We are unable to verify this since our free trial of Nabu Casa has expired.
|
||||||
|
|
||||||
Schema verification is a big part of this design choice. If the application cannot read your menu definition, there's a limited amount of debug it can reasonably provide on a small screen. That responsibility now falls to you and the schema checker for help.
|
Schema verification is a big part of this design choice. If the application cannot read your menu definition, there's a limited amount of debug it can reasonably provide on a small screen. That responsibility now falls to you and the schema checker for help.
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ rem
|
|||||||
rem -----------------------------------------------------------------------------------
|
rem -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
rem Check this path is correct for your Java installation
|
rem Check this path is correct for your Java installation
|
||||||
set JAVA_PATH=C:\Program Files (x86)\Common Files\Oracle\Java\javapath
|
set JAVA_PATH=C:\Program Files\Java\jdk-22\bin
|
||||||
rem SDK_PATH should work for all users
|
rem SDK_PATH should work for all users
|
||||||
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
|
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
|
||||||
set SDK_PATH=%SDK_PATH:~0,-1%\bin
|
set SDK_PATH=%SDK_PATH:~0,-1%\bin
|
||||||
|
@ -16,7 +16,6 @@ In this example we get the battery level of the device and add the percent sign.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"entity": "sensor.<device>_battery_level",
|
|
||||||
"name": "Phone",
|
"name": "Phone",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.<device>_battery_level') }}%"
|
"content": "{{ states('sensor.<device>_battery_level') }}%"
|
||||||
@ -29,19 +28,16 @@ The first two keep to the simple proposal above. The last combines them into a s
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"entity": "sensor.hallway_temperature",
|
|
||||||
"name": "Hall Temp",
|
"name": "Hall Temp",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.hallway_temperature') }}°C"
|
"content": "{{ states('sensor.hallway_temperature') }}°C"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity": "sensor.hallway_humidity",
|
|
||||||
"name": "Hall Humidity",
|
"name": "Hall Humidity",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.hallway_humidity') }}%"
|
"content": "{{ states('sensor.hallway_humidity') }}%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"entity": "sensor.hallway_temperature",
|
|
||||||
"name": "Hallway",
|
"name": "Hallway",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.hallway_temperature') }}°C {{ states('sensor.hallway_humidity') }}%"
|
"content": "{{ states('sensor.hallway_temperature') }}°C {{ states('sensor.hallway_humidity') }}%"
|
||||||
@ -61,16 +57,16 @@ In order to keep the formatting of floating point numbers under control, you mig
|
|||||||
Where your device supports unicode characters these example may work.
|
Where your device supports unicode characters these example may work.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Charge",
|
"name": "Charge",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "☎ {{ states('sensor.my_phone_battery_level') }}%{% if is_state('binary_sensor.my_phone_is_charging', 'on') %}⚡{% endif %}, ⏳ {{ '%.0f'|format(states('sensor.my_watch_battery_level') | float) }}%{% if is_state('binary_binary_sensor.my_watch_battery_is_charging', 'on') %}⚡{% endif %}"
|
"content": "☎ {{ states('sensor.my_phone_battery_level') }}%{% if is_state('binary_sensor.my_phone_is_charging', 'on') %}⚡{% endif %}, ⏳ {{ '%.0f'|format(states('sensor.my_watch_battery_level') | float) }}%{% if is_state('binary_binary_sensor.my_watch_battery_is_charging', 'on') %}⚡{% endif %}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Hallway",
|
"name": "Hallway",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "🌡{% if is_state('sensor.hallway_temperature', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_temperature')|float) }}°C{% if is_state_attr('climate.hallway', 'hvac_action', 'heating') or is_state_attr('climate.hallway', 'hvac_action', 'preheating') -%}🔥{%- endif %}{% endif %}, 💧{% if is_state('sensor.hallway_humidity', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_humidity')|float) }}%{% endif %}"
|
"content": "🌡{% if is_state('sensor.hallway_temperature', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_temperature')|float) }}°C{% if is_state_attr('climate.hallway', 'hvac_action', 'heating') or is_state_attr('climate.hallway', 'hvac_action', 'preheating') -%}🔥{%- endif %}{% endif %}, 💧{% if is_state('sensor.hallway_humidity', 'unavailable') %}-{% else %}{{ '%.1f'|format(states('sensor.hallway_humidity')|float) }}%{% endif %}"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
@ -83,7 +79,6 @@ In this example we get the battery level of the device and add the percent sign.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"entity": "sensor.<device>_battery_level",
|
|
||||||
"name": "Phone",
|
"name": "Phone",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %}"
|
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %}"
|
||||||
@ -94,13 +89,32 @@ Here we also use the else clause as well to give proper text instead of just `on
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"entity": "binary_sensor.garage_doors",
|
|
||||||
"name": "Garage Doors",
|
"name": "Garage Doors",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{% if is_state('binary_sensor.<door-0>', 'on') %}Open{% else %}Closed{% endif %} {% if is_state('binary_sensor.<door-1>', 'on') %}Open{% else %}Closed{% endif %}"
|
"content": "{% if is_state('binary_sensor.<door-0>', 'on') %}Open{% else %}Closed{% endif %} {% if is_state('binary_sensor.<door-1>', 'on') %}Open{% else %}Closed{% endif %}"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> We advise users against adding security devices.
|
||||||
|
|
||||||
|
However, users are doing this **against our advice** and asking how to operate 'covers'. This is an example of toggling a garage door open and closed with confirmation. *Do this at your own risk*.
|
||||||
|
|
||||||
|
Note: Only when you use the `tap_action` field do you also need to include the `entity` field. This is a change to a previous version of the application, hence the presence of the `entity` field will be ignored for backwards compatibility, and the schema will provide a warning only.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entity": "cover.garage_door",
|
||||||
|
"name": "Garage Door",
|
||||||
|
"type": "template",
|
||||||
|
"content": "{% if is_state('binary_sensor.garage_connected', 'on') %}{{state_translated('cover.garage_door')}} - {{state_attr('cover.garage_door', 'current_position')}}%{%else%}Unconnected{% endif %}",
|
||||||
|
"tap_action": {
|
||||||
|
"service": "cover.toggle",
|
||||||
|
"confirm": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
Here we generate a bar graph of the battery level. We use the following steps to do this:
|
Here we generate a bar graph of the battery level. We use the following steps to do this:
|
||||||
@ -114,7 +128,6 @@ Here we generate a bar graph of the battery level. We use the following steps to
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"entity": "sensor.<device>_battery_level",
|
|
||||||
"name": "Phone",
|
"name": "Phone",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %} {{ '#' * (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int) }}{{ '_' * (<width> - (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int)) }}"
|
"content": "{{ states('sensor.<device>_battery_level') }}%{% if is_state('binary_sensor.<device>_is_charging', 'on') %}+{% endif %} {{ '#' * (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int) }}{{ '_' * (<width> - (((states('sensor.<device>_battery_level') | int) / 100 * <width>) | int)) }}"
|
||||||
@ -125,11 +138,10 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"$schema": "./schema.json",
|
"$schema": "https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json",
|
||||||
"title": "Home",
|
"title": "Home",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"entity": "light.green_house",
|
|
||||||
"name": "LEDs",
|
"name": "LEDs",
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}"
|
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}"
|
||||||
@ -187,4 +199,4 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
|||||||
|
|
||||||
## Warnings
|
## Warnings
|
||||||
|
|
||||||
Just remember, **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.
|
||||||
|
@ -23,6 +23,7 @@ using Toybox.Lang;
|
|||||||
using Toybox.Application.Properties;
|
using Toybox.Application.Properties;
|
||||||
using Toybox.Background;
|
using Toybox.Background;
|
||||||
using Toybox.System;
|
using Toybox.System;
|
||||||
|
using Toybox.Activity;
|
||||||
|
|
||||||
(:background)
|
(:background)
|
||||||
class BackgroundServiceDelegate extends System.ServiceDelegate {
|
class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||||
@ -37,99 +38,81 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
|||||||
Background.exit(null);
|
Background.exit(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onActivityCompleted(activity as { :sport as Activity.Sport, :subSport as Activity.SubSport }) as Void {
|
||||||
|
if (!System.getDeviceSettings().phoneConnected) {
|
||||||
|
// System.println("BackgroundServiceDelegate onActivityCompleted(): No Phone connection, skipping API call.");
|
||||||
|
} else if (!System.getDeviceSettings().connectionAvailable) {
|
||||||
|
// System.println("BackgroundServiceDelegate onActivityCompleted(): No Internet connection, skipping API call.");
|
||||||
|
} else {
|
||||||
|
// Ensure we're logging completion, i.e. ignore 'activity' parameter
|
||||||
|
// System.println("BackgroundServiceDelegate onActivityCompleted(): Event triggered");
|
||||||
|
doUpdate(-1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onTemporalEvent() as Void {
|
function onTemporalEvent() as Void {
|
||||||
if (!System.getDeviceSettings().phoneConnected) {
|
if (!System.getDeviceSettings().phoneConnected) {
|
||||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call.");
|
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call.");
|
||||||
} else if (!System.getDeviceSettings().connectionAvailable) {
|
} else if (!System.getDeviceSettings().connectionAvailable) {
|
||||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Internet connection, skipping API call.");
|
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Internet connection, skipping API call.");
|
||||||
} else {
|
} else {
|
||||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
|
var activity = null;
|
||||||
var position = Position.getInfo();
|
var sub_activity = null;
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Don't use Settings.* here as the object lasts < 30 secs and is recreated each time the background service is run
|
|
||||||
|
|
||||||
if (position.accuracy != Position.QUALITY_NOT_AVAILABLE && position.accuracy != Position.QUALITY_LAST_KNOWN) {
|
|
||||||
var accuracy = 0;
|
|
||||||
switch (position.accuracy) {
|
|
||||||
case Position.QUALITY_POOR:
|
|
||||||
accuracy = 500;
|
|
||||||
break;
|
|
||||||
case Position.QUALITY_USABLE:
|
|
||||||
accuracy = 100;
|
|
||||||
break;
|
|
||||||
case Position.QUALITY_GOOD:
|
|
||||||
accuracy = 10;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
|
||||||
:headers => {
|
|
||||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
|
||||||
},
|
|
||||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
|
||||||
},
|
|
||||||
method(:onReturnBatteryUpdate)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var data = [
|
|
||||||
{
|
|
||||||
"state" => System.getSystemStats().battery,
|
|
||||||
"type" => "sensor",
|
|
||||||
"unique_id" => "battery_level"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"state" => System.getSystemStats().charging,
|
|
||||||
"type" => "binary_sensor",
|
|
||||||
"unique_id" => "battery_is_charging"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
if ((Activity has :getActivityInfo) and (Activity has :getProfileInfo)) {
|
if ((Activity has :getActivityInfo) and (Activity has :getProfileInfo)) {
|
||||||
var activity = Activity.getProfileInfo().sport;
|
activity = Activity.getProfileInfo().sport;
|
||||||
var sub_activity = Activity.getProfileInfo().subSport;
|
sub_activity = Activity.getProfileInfo().subSport;
|
||||||
// We need to check if we are actually tracking any activity as the enumerated type does not include "No Sport".
|
// We need to check if we are actually tracking any activity as the enumerated type does not include "No Sport".
|
||||||
if ((Activity.getActivityInfo() != null) and
|
if ((Activity.getActivityInfo() != null) and
|
||||||
((Activity.getActivityInfo().elapsedTime == null) or
|
((Activity.getActivityInfo().elapsedTime == null) or
|
||||||
(Activity.getActivityInfo().elapsedTime == 0))) {
|
(Activity.getActivityInfo().elapsedTime == 0))) {
|
||||||
// Indicate no activity with -1, not part of Garmin's activity codes.
|
// 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
|
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
|
||||||
activity = -1;
|
activity = -1;
|
||||||
sub_activity = -1;
|
sub_activity = -1;
|
||||||
}
|
}
|
||||||
data.add({
|
}
|
||||||
"state" => activity,
|
// System.println("BackgroundServiceDelegate onTemporalEvent(): Event triggered, activity = " + activity + " sub_activity = " + sub_activity);
|
||||||
"type" => "sensor",
|
doUpdate(activity, sub_activity);
|
||||||
"unique_id" => "activity"
|
}
|
||||||
});
|
}
|
||||||
data.add({
|
|
||||||
"state" => sub_activity,
|
private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) {
|
||||||
"type" => "sensor",
|
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
|
||||||
"unique_id" => "sub_activity"
|
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);
|
||||||
|
|
||||||
|
// Don't use Settings.* here as the object lasts < 30 secs and is recreated each time the background service is run
|
||||||
|
|
||||||
|
if (position.accuracy != Position.QUALITY_NOT_AVAILABLE && position.accuracy != Position.QUALITY_LAST_KNOWN) {
|
||||||
|
var accuracy = 0;
|
||||||
|
switch (position.accuracy) {
|
||||||
|
case Position.QUALITY_POOR:
|
||||||
|
accuracy = 500;
|
||||||
|
break;
|
||||||
|
case Position.QUALITY_USABLE:
|
||||||
|
accuracy = 100;
|
||||||
|
break;
|
||||||
|
case Position.QUALITY_GOOD:
|
||||||
|
accuracy = 10;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
(Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String),
|
(Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String),
|
||||||
{
|
{
|
||||||
"type" => "update_sensor_states",
|
"type" => "update_location",
|
||||||
"data" => data
|
"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),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||||
@ -141,6 +124,47 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
|||||||
method(:onReturnBatteryUpdate)
|
method(:onReturnBatteryUpdate)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
var data = [
|
||||||
|
{
|
||||||
|
"state" => System.getSystemStats().battery,
|
||||||
|
"type" => "sensor",
|
||||||
|
"unique_id" => "battery_level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state" => System.getSystemStats().charging,
|
||||||
|
"type" => "binary_sensor",
|
||||||
|
"unique_id" => "battery_is_charging"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (activity != null) {
|
||||||
|
data.add({
|
||||||
|
"state" => activity,
|
||||||
|
"type" => "sensor",
|
||||||
|
"unique_id" => "activity"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sub_activity != null) {
|
||||||
|
data.add({
|
||||||
|
"state" => sub_activity,
|
||||||
|
"type" => "sensor",
|
||||||
|
"unique_id" => "sub_activity"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Communications.makeWebRequest(
|
||||||
|
(Properties.getValue("api_url") as Lang.String) + "/webhook/" + (Properties.getValue("webhook_id") as Lang.String),
|
||||||
|
{
|
||||||
|
"type" => "update_sensor_states",
|
||||||
|
"data" => data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||||
|
:headers => {
|
||||||
|
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||||
|
},
|
||||||
|
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||||
|
},
|
||||||
|
method(:onReturnBatteryUpdate)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -27,17 +27,18 @@ 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 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 mQuitTimer as QuitTimer or Null;
|
private var mQuitTimer as QuitTimer or Null;
|
||||||
private var mGlanceTimer as Timer.Timer or Null;
|
private var mGlanceTimer as Timer.Timer or Null;
|
||||||
private var mUpdateTimer as Timer.Timer or Null;
|
private var mUpdateTimer as Timer.Timer or Null;
|
||||||
// Array initialised by onReturnFetchMenuConfig()
|
// Array initialised by onReturnFetchMenuConfig()
|
||||||
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> or Null;
|
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 mNextItemToUpdate as Lang.Number = 0; // Index into the above array
|
||||||
private var mIsGlance as Lang.Boolean = false;
|
private var mIsGlance as Lang.Boolean = false;
|
||||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
private var mIsApp as Lang.Boolean = false; // Or Widget
|
||||||
|
private var mIsInitUpdateCompl as Lang.Boolean = false;
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
AppBase.initialize();
|
AppBase.initialize();
|
||||||
@ -204,6 +205,7 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
// asynchronous and affects how the views are managed.
|
// asynchronous and affects how the views are managed.
|
||||||
(:glance)
|
(:glance)
|
||||||
function fetchMenuConfig() as Lang.Boolean {
|
function fetchMenuConfig() as Lang.Boolean {
|
||||||
|
// System.println("URL = " + Settings.getConfigUrl());
|
||||||
if (Settings.getConfigUrl().equals("")) {
|
if (Settings.getConfigUrl().equals("")) {
|
||||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||||
WatchUi.requestUpdate();
|
WatchUi.requestUpdate();
|
||||||
@ -257,6 +259,9 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
private function buildMenu(menu as Lang.Dictionary) {
|
private function buildMenu(menu as Lang.Dictionary) {
|
||||||
mHaMenu = new HomeAssistantView(menu, null);
|
mHaMenu = new HomeAssistantView(menu, null);
|
||||||
mQuitTimer.begin();
|
mQuitTimer.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startUpdates() {
|
||||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||||
// 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.
|
||||||
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
|
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
|
||||||
@ -396,20 +401,42 @@ class HomeAssistantApp extends Application.AppBase {
|
|||||||
|
|
||||||
function updateNextMenuItem() as Void {
|
function updateNextMenuItem() as Void {
|
||||||
var delay = Settings.getPollDelay();
|
var delay = Settings.getPollDelay();
|
||||||
if ((delay > 0) and (mNextItemToUpdate == 0)) {
|
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
|
||||||
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
|
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
|
||||||
} else {
|
} else {
|
||||||
updateNextMenuItemInternal();
|
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
|
// 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.
|
// (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms.
|
||||||
function updateNextMenuItemInternal() as Void {
|
function updateNextMenuItemInternal() as Void {
|
||||||
var itu = mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem>;
|
var itu = mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem>;
|
||||||
if (itu != null) {
|
if (itu != null) {
|
||||||
|
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
|
||||||
itu[mNextItemToUpdate].getState();
|
itu[mNextItemToUpdate].getState();
|
||||||
mNextItemToUpdate = (mNextItemToUpdate + 1) % itu.size();
|
// 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 {
|
// } else {
|
||||||
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
|
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ class HomeAssistantService {
|
|||||||
|
|
||||||
case 200:
|
case 200:
|
||||||
// System.println("HomeAssistantService onReturnCall(): Service executed.");
|
// System.println("HomeAssistantService onReturnCall(): Service executed.");
|
||||||
|
getApp().forceStatusUpdates();
|
||||||
var d = data as Lang.Array;
|
var d = data as Lang.Array;
|
||||||
var toast = WatchUi.loadResource($.Rez.Strings.Executed) as Lang.String;
|
var toast = WatchUi.loadResource($.Rez.Strings.Executed) as Lang.String;
|
||||||
for(var i = 0; i < d.size(); i++) {
|
for(var i = 0; i < d.size(); i++) {
|
||||||
|
@ -83,7 +83,7 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
|||||||
// Terminate updating the toggle menu items via the chain of calls for a permanent network
|
// Terminate updating the toggle menu items via the chain of calls for a permanent network
|
||||||
// error. The ErrorView cancellation will resume the call chain.
|
// error. The ErrorView cancellation will resume the call chain.
|
||||||
//
|
//
|
||||||
function onReturnGetState(responseCode as Lang.Number, data as Lang.String) as Void {
|
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 Code: " + responseCode);
|
||||||
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
|
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
|||||||
|
|
||||||
case 200:
|
case 200:
|
||||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||||
setSubLabel(data);
|
setSubLabel(data.get("request"));
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||||
getApp().updateNextMenuItem();
|
getApp().updateNextMenuItem();
|
||||||
@ -153,19 +153,28 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
|||||||
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
|
// System.println("HomeAssistantTemplateMenuItem getState(): 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 + ".");
|
||||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||||
|
} else if (Settings.getWebhookId().equals("")) {
|
||||||
|
getApp().updateNextMenuItem();
|
||||||
} else {
|
} else {
|
||||||
var url = Settings.getApiUrl() + "/template";
|
// 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 + "'");
|
// System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
url,
|
url,
|
||||||
{ "template" => mTemplate },
|
|
||||||
{
|
{
|
||||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
"type" => "render_template",
|
||||||
:headers => {
|
"data" => {
|
||||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
|
"request" => {
|
||||||
"Authorization" => "Bearer " + Settings.getApiKey()
|
"template" => mTemplate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||||
|
:headers => {
|
||||||
|
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||||
},
|
},
|
||||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_TEXT_PLAIN
|
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||||
},
|
},
|
||||||
method(:onReturnGetState)
|
method(:onReturnGetState)
|
||||||
);
|
);
|
||||||
|
@ -196,6 +196,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 200:
|
case 200:
|
||||||
|
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
||||||
|
getApp().forceStatusUpdates();
|
||||||
var state;
|
var state;
|
||||||
var d = data as Lang.Array;
|
var d = data as Lang.Array;
|
||||||
for(var i = 0; i < d.size(); i++) {
|
for(var i = 0; i < d.size(); i++) {
|
||||||
|
@ -39,7 +39,7 @@ class Settings {
|
|||||||
private static var mPollDelay as Lang.Number = 0; // seconds
|
private static var mPollDelay as Lang.Number = 0; // seconds
|
||||||
private static var mConfirmTimeout as Lang.Number = 3; // seconds
|
private static var mConfirmTimeout as Lang.Number = 3; // seconds
|
||||||
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
|
private static var mMenuAlignment as Lang.Number = WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT;
|
||||||
private static var mIsBatteryLevelEnabled as Lang.Boolean = false;
|
private static var mIsSensorsLevelEnabled as Lang.Boolean = false;
|
||||||
private static var mBatteryRefreshRate as Lang.Number = 15; // minutes
|
private static var mBatteryRefreshRate as Lang.Number = 15; // minutes
|
||||||
private static var mIsApp as Lang.Boolean = false;
|
private static var mIsApp as Lang.Boolean = false;
|
||||||
private static var mHasService as Lang.Boolean = false;
|
private static var mHasService as Lang.Boolean = false;
|
||||||
@ -60,7 +60,7 @@ class Settings {
|
|||||||
mPollDelay = Properties.getValue("poll_delay");
|
mPollDelay = Properties.getValue("poll_delay");
|
||||||
mConfirmTimeout = Properties.getValue("confirm_timeout");
|
mConfirmTimeout = Properties.getValue("confirm_timeout");
|
||||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||||
mIsBatteryLevelEnabled = 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");
|
||||||
|
|
||||||
if (System has :ServiceDelegate) {
|
if (System has :ServiceDelegate) {
|
||||||
@ -69,19 +69,42 @@ class Settings {
|
|||||||
|
|
||||||
// Manage this inside the application or widget only (not a glance or background service process)
|
// Manage this inside the application or widget only (not a glance or background service process)
|
||||||
if (mIsApp) {
|
if (mIsApp) {
|
||||||
if (mIsBatteryLevelEnabled and mHasService) {
|
if (mHasService) {
|
||||||
|
mWebhookManager = new WebhookManager();
|
||||||
if (getWebhookId().equals("")) {
|
if (getWebhookId().equals("")) {
|
||||||
mWebhookManager = new WebhookManager();
|
// System.println("Settings update(): Doing full webhook & sensor creation.");
|
||||||
mWebhookManager.requestWebhookId();
|
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 ((Background.getTemporalEventRegisteredTime() == null) or
|
if (mIsSensorsLevelEnabled) {
|
||||||
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
|
// Create the timed activity
|
||||||
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
|
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 {
|
} else {
|
||||||
// Explicitly disable the background event which persists when the application closes.
|
// Explicitly disable the background event which persists when the application closes.
|
||||||
// If !mHasService disable the Settings option as user feedback
|
// If !mHasService disable the Settings option as user feedback
|
||||||
unsetIsBatteryLevelEnabled();
|
unsetIsSensorsLevelEnabled();
|
||||||
unsetWebhookId();
|
unsetWebhookId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,11 +175,16 @@ class Settings {
|
|||||||
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
|
return mMenuAlignment; // Either WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_RIGHT or WatchUi.MenuItem.MENU_ITEM_LABEL_ALIGN_LEFT
|
||||||
}
|
}
|
||||||
|
|
||||||
static function unsetIsBatteryLevelEnabled() {
|
static function isSensorsLevelEnabled() as Lang.Boolean {
|
||||||
mIsBatteryLevelEnabled = false;
|
return mIsSensorsLevelEnabled;
|
||||||
Properties.setValue("enable_battery_level", mIsBatteryLevelEnabled);
|
}
|
||||||
|
|
||||||
|
static function unsetIsSensorsLevelEnabled() {
|
||||||
|
mIsSensorsLevelEnabled = false;
|
||||||
|
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);
|
||||||
if (mHasService and (Background.getTemporalEventRegisteredTime() != null)) {
|
if (mHasService and (Background.getTemporalEventRegisteredTime() != null)) {
|
||||||
Background.deleteTemporalEvent();
|
Background.deleteTemporalEvent();
|
||||||
|
Background.deleteActivityCompletedEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
using Toybox.Lang;
|
using Toybox.Lang;
|
||||||
using Toybox.Communications;
|
using Toybox.Communications;
|
||||||
using Toybox.System;
|
using Toybox.System;
|
||||||
|
using Toybox.WatchUi;
|
||||||
|
|
||||||
// Can use push view so must never be run in a glance context
|
// Can use push view so must never be run in a glance context
|
||||||
class WebhookManager {
|
class WebhookManager {
|
||||||
@ -52,13 +53,13 @@ class WebhookManager {
|
|||||||
break;
|
break;
|
||||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
Settings.unsetIsSensorsLevelEnabled();
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
|
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
Settings.unsetIsSensorsLevelEnabled();
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -77,25 +78,25 @@ class WebhookManager {
|
|||||||
"unit_of_measurement" => "%",
|
"unit_of_measurement" => "%",
|
||||||
"state_class" => "measurement",
|
"state_class" => "measurement",
|
||||||
"entity_category" => "diagnostic",
|
"entity_category" => "diagnostic",
|
||||||
"disabled" => false
|
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
|
// System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
Settings.unsetIsSensorsLevelEnabled();
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
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 requestWebhookId() {
|
function requestWebhookId() {
|
||||||
// System.println("WebhookManager requestWebhookId(): Requesting webhook id");
|
|
||||||
var deviceSettings = System.getDeviceSettings();
|
var deviceSettings = System.getDeviceSettings();
|
||||||
|
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
Settings.getApiUrl() + "/mobile_app/registrations",
|
Settings.getApiUrl() + "/mobile_app/registrations",
|
||||||
{
|
{
|
||||||
@ -153,21 +154,24 @@ class WebhookManager {
|
|||||||
|
|
||||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||||
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
|
||||||
|
// Webhook ID might have been deleted on Home Assistant server
|
||||||
Settings.unsetWebhookId();
|
Settings.unsetWebhookId();
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
requestWebhookId();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
|
// System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: 404, page not found. Check API URL setting.");
|
||||||
|
// Webhook ID might have been deleted on Home Assistant server
|
||||||
Settings.unsetWebhookId();
|
Settings.unsetWebhookId();
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
requestWebhookId();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 200:
|
case 200:
|
||||||
case 201:
|
case 201:
|
||||||
if ((data.get("success") as Lang.Boolean or Null) != false) {
|
var d = data as Lang.Dictionary;
|
||||||
|
if ((d.get("success") as Lang.Boolean or Null) != false) {
|
||||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Success");
|
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Success");
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -179,7 +183,7 @@ class WebhookManager {
|
|||||||
"type" => "binary_sensor",
|
"type" => "binary_sensor",
|
||||||
"unique_id" => "battery_is_charging",
|
"unique_id" => "battery_is_charging",
|
||||||
"entity_category" => "diagnostic",
|
"entity_category" => "diagnostic",
|
||||||
"disabled" => false
|
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||||
}, 1);
|
}, 1);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
@ -198,12 +202,12 @@ class WebhookManager {
|
|||||||
"state" => activity,
|
"state" => activity,
|
||||||
"type" => "sensor",
|
"type" => "sensor",
|
||||||
"unique_id" => "activity",
|
"unique_id" => "activity",
|
||||||
"disabled" => false
|
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||||
}, 2);
|
}, 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2:
|
case 2:
|
||||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity");
|
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Sub-Activity");
|
||||||
if (Activity has :getProfileInfo) {
|
if (Activity has :getProfileInfo) {
|
||||||
var sub_activity = Activity.getProfileInfo().subSport;
|
var sub_activity = Activity.getProfileInfo().subSport;
|
||||||
if ((Activity.getActivityInfo() != null) and
|
if ((Activity.getActivityInfo() != null) and
|
||||||
@ -218,16 +222,18 @@ class WebhookManager {
|
|||||||
"state" => sub_activity,
|
"state" => sub_activity,
|
||||||
"type" => "sensor",
|
"type" => "sensor",
|
||||||
"unique_id" => "sub_activity",
|
"unique_id" => "sub_activity",
|
||||||
"disabled" => false
|
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||||
}, 3);
|
}, 3);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 3:
|
||||||
|
getApp().startUpdates();
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
|
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
|
||||||
Settings.unsetWebhookId();
|
Settings.unsetWebhookId();
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
Settings.unsetIsSensorsLevelEnabled();
|
||||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -235,15 +241,18 @@ class WebhookManager {
|
|||||||
default:
|
default:
|
||||||
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
||||||
Settings.unsetWebhookId();
|
Settings.unsetWebhookId();
|
||||||
Settings.unsetIsBatteryLevelEnabled();
|
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(sensor as Lang.Object, step as Lang.Number) {
|
||||||
|
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
|
||||||
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
|
// System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
|
||||||
|
// System.println("WebhookManager registerWebhookSensor(): URL=" + url);
|
||||||
|
// https://developers.home-assistant.io/docs/api/native-app-integration/sensors/#registering-a-sensor
|
||||||
Communications.makeWebRequest(
|
Communications.makeWebRequest(
|
||||||
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
|
url,
|
||||||
{
|
{
|
||||||
"type" => "register_sensor",
|
"type" => "register_sensor",
|
||||||
"data" => sensor
|
"data" => sensor
|
||||||
|
Reference in New Issue
Block a user