Compare commits

..

13 Commits
v2.12 ... v2.13

Author SHA1 Message Date
9030a00d7d Update BackgroundServiceDelegate.mc
Put check for methods with 'has' back in.
2024-07-22 22:18:27 +01:00
7bd5e98a1a More responsive status when using a polling delay (#153)
When using a polling delay, an action will trigger an immediate round of
updates.
2024-07-21 16:00:21 +00:00
a49dd6554f More responsive status when using a polling delay
When using a polling delay, an action will trigger an immediate round of updates.
2024-07-20 19:16:42 +01:00
d2fc64836a Update HomeAssistantApp.mc (#152)
When Settings.getPollDelay() > 0, do not wait for this time before the
initial batch of updates, so the app is more responsive on opening.
2024-07-20 17:09:23 +00:00
fc1a0eeb6d Update HomeAssistantApp.mc
When Settings.getPollDelay() > 0, do not wait for this time before the initial batch of updates, so the app is more responsive on opening.
2024-07-20 18:07:37 +01:00
61f6d69e64 Update HISTORY.md
Added a second feature to 2.13 in anticipation.
2024-07-20 17:39:59 +01:00
7f31cecfb5 Added update to telemetry on completion of an activity (#151)
@Someone0nEarth Note we could also add this to the widget version as the
changes are small and probably have a limited effect on the memory
footprint.
2024-07-20 16:37:29 +00:00
98af5578f0 Added update to telemetry on completion of an activity 2024-07-20 17:28:57 +01:00
c18736e40d Update Templates.md
Examples now up to date with schema changes.
2024-07-20 17:23:00 +01:00
822c6ccca6 Moved template status updates to webhooks (#150)
This seems to work for non-privileged users.
2024-07-20 15:49:04 +00:00
a139742265 Update HISTORY.md
Ver 2.13 text added in anticipation.
2024-07-19 17:05:19 +01:00
dfa4cdd9b8 Moved template status updates to webhooks
This seems to work for non-privileged users.
2024-07-19 15:15:32 +01:00
1c4add693d Update README.md
Amended advice based on a report at https://community.home-assistant.io/t/home-assistant-app-for-garmin/637348/280.

Signed-off-by: Philip Abbey <philipabbey@users.noreply.github.com>
2024-07-01 08:40:04 +01:00
11 changed files with 263 additions and 147 deletions

View File

@ -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. |

View File

@ -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.

View File

@ -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

View File

@ -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 %}"
} }
``` ```
![Unicode Characters in a Template](../images/Unicode_Template.png) ![Unicode Characters in a Template](../images/Unicode_Template.png)
@ -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.

View File

@ -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)
);
} }
} }

View File

@ -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");
} }

View File

@ -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++) {

View File

@ -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)
); );

View File

@ -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++) {

View File

@ -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();
} }
} }

View File

@ -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