mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-12-16 11:08:15 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9030a00d7d | ||
|
|
7bd5e98a1a | ||
|
|
a49dd6554f | ||
|
|
d2fc64836a | ||
|
|
fc1a0eeb6d | ||
|
|
61f6d69e64 | ||
|
|
7f31cecfb5 | ||
|
|
98af5578f0 | ||
|
|
c18736e40d | ||
|
|
822c6ccca6 | ||
|
|
a139742265 | ||
|
|
dfa4cdd9b8 | ||
|
|
1c4add693d | ||
|
|
e6544b5b20 | ||
|
|
83914e8d8d | ||
|
|
bd4b6d68a8 | ||
|
|
f4ca55d741 | ||
|
|
5d34db7b6a | ||
|
|
cfaa31d5ee | ||
|
|
52ee270788 |
@@ -24,3 +24,5 @@
|
||||
| 2.9 | Added an option to enable confirmation vibration so it can be turned off by request of a user. Removed a redundant setting for the alternative Widget version that was not removed previously, and fixed a bug with dereferencing Null. |
|
||||
| 2.10 | Added a user requested feature to slow down the rate of API calls in order to reduce battery wear for a situation where the application is kept open permanently on the device for convenience. Added 4 new devices. |
|
||||
| 2.11 | Bug fix release for menu caching being turned off and language corrections (Czech & Slovenian). |
|
||||
| 2.12 | Re-enabled Edge 540 and Edge 840 devices which we are unable to support due to simulator issues, but the Edge 840 device has been confirmed as working by a @Petucky. |
|
||||
| 2.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
|
||||
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.
|
||||
|
||||
@@ -308,4 +311,6 @@ The `id` attribute values are taken from the same names used in [`strings.xml`](
|
||||
|
||||
5. Parameters to tap menu items cannot have their parameter usage verified. If you get this wrong and crash the application, that's your fault not the application's. In this case, start by removing the parameters for the menu item causing the crash, and add them back one at a time until you find your fault. **Please don't give the application a poor review for your bad parameter definition!**
|
||||
|
||||
6. We are unable to support Edge 540 and Edge 840 devices at this time. The simulation of both these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. We would appreciate any leads others may have on how to solve this issue.
|
||||
6. We are unable to support Edge 540 and Edge 840 devices at this time. The simulation of both these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed.
|
||||
|
||||
7. We are unable to support HTTP without HTTPS. This is a limitation placed upon us by the Connect IQ API which for security reasons refuses to work with HTTP requests. There is nothing developers can do about this limitation. You will have to put an HTTPS proxy in front of your local Home Assistant to work with this application. See the [Trouble Shooting](TroubleShooting.md#do-it-yourself-setup) guide for an example setup. We would appreciate it if users did not leave poor reviews for the lack of this feature which is beyond our control to fix.
|
||||
|
||||
@@ -21,7 +21,7 @@ rem
|
||||
rem -----------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
set /p SDK_PATH=<"%USERPROFILE%\AppData\Roaming\Garmin\ConnectIQ\current-sdk.cfg"
|
||||
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
|
||||
{
|
||||
"entity": "sensor.<device>_battery_level",
|
||||
"name": "Phone",
|
||||
"type": "template",
|
||||
"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
|
||||
{
|
||||
"entity": "sensor.hallway_temperature",
|
||||
"name": "Hall Temp",
|
||||
"type": "template",
|
||||
"content": "{{ states('sensor.hallway_temperature') }}°C"
|
||||
},
|
||||
{
|
||||
"entity": "sensor.hallway_humidity",
|
||||
"name": "Hall Humidity",
|
||||
"type": "template",
|
||||
"content": "{{ states('sensor.hallway_humidity') }}%"
|
||||
},
|
||||
{
|
||||
"entity": "sensor.hallway_temperature",
|
||||
"name": "Hallway",
|
||||
"type": "template",
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"name": "Charge",
|
||||
"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 %}"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
"name": "Hallway",
|
||||
"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 %}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
@@ -83,7 +79,6 @@ In this example we get the battery level of the device and add the percent sign.
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "sensor.<device>_battery_level",
|
||||
"name": "Phone",
|
||||
"type": "template",
|
||||
"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
|
||||
{
|
||||
"entity": "binary_sensor.garage_doors",
|
||||
"name": "Garage Doors",
|
||||
"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 %}"
|
||||
}
|
||||
```
|
||||
|
||||
> [!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
|
||||
|
||||
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
|
||||
{
|
||||
"entity": "sensor.<device>_battery_level",
|
||||
"name": "Phone",
|
||||
"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)) }}"
|
||||
@@ -125,11 +138,10 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "./schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json",
|
||||
"title": "Home",
|
||||
"items": [
|
||||
{
|
||||
"entity": "light.green_house",
|
||||
"name": "LEDs",
|
||||
"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 %}"
|
||||
@@ -187,4 +199,4 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -56,8 +56,10 @@
|
||||
<iq:product id="edge1040" />
|
||||
<iq:product id="edge520plus" />
|
||||
<iq:product id="edge530" />
|
||||
<iq:product id="edge540" />
|
||||
<iq:product id="edge820" />
|
||||
<iq:product id="edge830" />
|
||||
<iq:product id="edge840" />
|
||||
<iq:product id="edgeexplore" />
|
||||
<iq:product id="edgeexplore2" />
|
||||
<iq:product id="enduro" />
|
||||
|
||||
@@ -68,10 +68,12 @@ edge1040.resourcePath = $(edge1040.resourcePath);resources-launcher-40-40;resour
|
||||
edge520plus.resourcePath = $(edge520plus.resourcePath);resources-launcher-35-35;resources-icons-24
|
||||
# Screen Size 246x322 launcher icon size 35x35
|
||||
edge530.resourcePath = $(edge530.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
edge540.resourcePath = $(edge540.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
# Screen Size 200x265 launcher icon size 35x35
|
||||
edge820.resourcePath = $(edge820.resourcePath);resources-launcher-35-35;resources-icons-24
|
||||
# Screen Size 246x322 launcher icon size 35x35
|
||||
edge830.resourcePath = $(edge830.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
edge840.resourcePath = $(edge840.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
# Screen Size 240x400 launcher icon size 36x36
|
||||
edgeexplore.resourcePath = $(edgeexplore.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
edgeexplore2.resourcePath = $(edgeexplore2.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
|
||||
@@ -23,6 +23,7 @@ using Toybox.Lang;
|
||||
using Toybox.Application.Properties;
|
||||
using Toybox.Background;
|
||||
using Toybox.System;
|
||||
using Toybox.Activity;
|
||||
|
||||
(:background)
|
||||
class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
@@ -37,12 +38,45 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
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 {
|
||||
if (!System.getDeviceSettings().phoneConnected) {
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Phone connection, skipping API call.");
|
||||
} else if (!System.getDeviceSettings().connectionAvailable) {
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): No Internet connection, skipping API call.");
|
||||
} else {
|
||||
var activity = null;
|
||||
var sub_activity = null;
|
||||
if ((Activity has :getActivityInfo) and (Activity has :getProfileInfo)) {
|
||||
activity = Activity.getProfileInfo().sport;
|
||||
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".
|
||||
if ((Activity.getActivityInfo() != null) and
|
||||
((Activity.getActivityInfo().elapsedTime == null) or
|
||||
(Activity.getActivityInfo().elapsedTime == 0))) {
|
||||
// Indicate no activity with -1, not part of Garmin's activity codes.
|
||||
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
|
||||
activity = -1;
|
||||
sub_activity = -1;
|
||||
}
|
||||
}
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): Event triggered, activity = " + activity + " sub_activity = " + sub_activity);
|
||||
doUpdate(activity, sub_activity);
|
||||
}
|
||||
}
|
||||
|
||||
private function doUpdate(activity as Lang.Number or Null, sub_activity as Lang.Number or Null) {
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): Making API call.");
|
||||
var position = Position.getInfo();
|
||||
// System.println("BackgroundServiceDelegate onTemporalEvent(): gps: " + position.position.toDegrees());
|
||||
@@ -102,23 +136,14 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
"unique_id" => "battery_is_charging"
|
||||
}
|
||||
];
|
||||
if ((Activity has :getActivityInfo) and (Activity has :getProfileInfo)) {
|
||||
var activity = Activity.getProfileInfo().sport;
|
||||
var 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".
|
||||
if ((Activity.getActivityInfo() != null) and
|
||||
((Activity.getActivityInfo().elapsedTime == null) or
|
||||
(Activity.getActivityInfo().elapsedTime == 0))) {
|
||||
// Indicate no activity with -1, not part of Garmin's activity codes.
|
||||
// https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
|
||||
activity = -1;
|
||||
sub_activity = -1;
|
||||
}
|
||||
if (activity != null) {
|
||||
data.add({
|
||||
"state" => activity,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "activity"
|
||||
});
|
||||
}
|
||||
if (sub_activity != null) {
|
||||
data.add({
|
||||
"state" => sub_activity,
|
||||
"type" => "sensor",
|
||||
@@ -141,6 +166,5 @@ class BackgroundServiceDelegate extends System.ServiceDelegate {
|
||||
method(:onReturnBatteryUpdate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
private var mNextItemToUpdate as Lang.Number = 0; // Index into the above array
|
||||
private var mIsGlance as Lang.Boolean = false;
|
||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
||||
private var mIsInitUpdateCompl as Lang.Boolean = false;
|
||||
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
@@ -204,6 +205,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
// asynchronous and affects how the views are managed.
|
||||
(:glance)
|
||||
function fetchMenuConfig() as Lang.Boolean {
|
||||
// System.println("URL = " + Settings.getConfigUrl());
|
||||
if (Settings.getConfigUrl().equals("")) {
|
||||
mMenuStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String;
|
||||
WatchUi.requestUpdate();
|
||||
@@ -257,6 +259,9 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
private function buildMenu(menu as Lang.Dictionary) {
|
||||
mHaMenu = new HomeAssistantView(menu, null);
|
||||
mQuitTimer.begin();
|
||||
}
|
||||
|
||||
function startUpdates() {
|
||||
mItemsToUpdate = mHaMenu.getItemsToUpdate();
|
||||
// Start the continuous update process that continues for as long as the application is running.
|
||||
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
|
||||
@@ -396,20 +401,42 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
|
||||
function updateNextMenuItem() as Void {
|
||||
var delay = Settings.getPollDelay();
|
||||
if ((delay > 0) and (mNextItemToUpdate == 0)) {
|
||||
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
|
||||
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
|
||||
} else {
|
||||
updateNextMenuItemInternal();
|
||||
}
|
||||
}
|
||||
|
||||
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take
|
||||
// alternative action if the test fails.
|
||||
function forceStatusUpdates() as Void {
|
||||
// Don't mess with updates unless we are using a timer.
|
||||
if (Settings.getPollDelay() > 0) {
|
||||
mUpdateTimer.stop();
|
||||
mIsInitUpdateCompl = false;
|
||||
// Start from the beginning, or we will only get a partial round of updates before mIsInitUpdateCompl is flipped.
|
||||
mNextItemToUpdate = 0;
|
||||
// For immediate updates
|
||||
updateNextMenuItem();
|
||||
}
|
||||
}
|
||||
|
||||
// We need to spread out the API calls so as not to overload the results queue and cause Communications.BLE_QUEUE_FULL
|
||||
// (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms.
|
||||
function updateNextMenuItemInternal() as Void {
|
||||
var itu = mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem>;
|
||||
if (itu != null) {
|
||||
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
|
||||
itu[mNextItemToUpdate].getState();
|
||||
mNextItemToUpdate = (mNextItemToUpdate + 1) % itu.size();
|
||||
// mNextItemToUpdate = (mNextItemToUpdate + 1) % itu.size() - But with roll-over detection
|
||||
if (mNextItemToUpdate == itu.size()-1) {
|
||||
// Last item completed return to the start of the list
|
||||
mNextItemToUpdate = 0;
|
||||
mIsInitUpdateCompl = true;
|
||||
} else {
|
||||
mNextItemToUpdate++;
|
||||
}
|
||||
// } else {
|
||||
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class HomeAssistantService {
|
||||
|
||||
case 200:
|
||||
// System.println("HomeAssistantService onReturnCall(): Service executed.");
|
||||
getApp().forceStatusUpdates();
|
||||
var d = data as Lang.Array;
|
||||
var toast = WatchUi.loadResource($.Rez.Strings.Executed) as Lang.String;
|
||||
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
|
||||
// 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 Data: " + data);
|
||||
|
||||
@@ -131,7 +131,7 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
||||
|
||||
case 200:
|
||||
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
setSubLabel(data);
|
||||
setSubLabel(data.get("request"));
|
||||
requestUpdate();
|
||||
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
|
||||
getApp().updateNextMenuItem();
|
||||
@@ -153,19 +153,28 @@ class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
|
||||
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
|
||||
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
|
||||
} else if (Settings.getWebhookId().equals("")) {
|
||||
getApp().updateNextMenuItem();
|
||||
} 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 + "'");
|
||||
Communications.makeWebRequest(
|
||||
url,
|
||||
{ "template" => mTemplate },
|
||||
{
|
||||
"type" => "render_template",
|
||||
"data" => {
|
||||
"request" => {
|
||||
"template" => mTemplate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
:method => Communications.HTTP_REQUEST_METHOD_POST,
|
||||
:headers => {
|
||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON,
|
||||
"Authorization" => "Bearer " + Settings.getApiKey()
|
||||
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
|
||||
},
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_TEXT_PLAIN
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
},
|
||||
method(:onReturnGetState)
|
||||
);
|
||||
|
||||
@@ -196,6 +196,8 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
break;
|
||||
|
||||
case 200:
|
||||
// System.println("HomeAssistantToggleMenuItem onReturnSetState(): Service executed.");
|
||||
getApp().forceStatusUpdates();
|
||||
var state;
|
||||
var d = data as Lang.Array;
|
||||
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 mConfirmTimeout as Lang.Number = 3; // seconds
|
||||
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 mIsApp as Lang.Boolean = false;
|
||||
private static var mHasService as Lang.Boolean = false;
|
||||
@@ -60,7 +60,7 @@ class Settings {
|
||||
mPollDelay = Properties.getValue("poll_delay");
|
||||
mConfirmTimeout = Properties.getValue("confirm_timeout");
|
||||
mMenuAlignment = Properties.getValue("menu_alignment");
|
||||
mIsBatteryLevelEnabled = Properties.getValue("enable_battery_level");
|
||||
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");
|
||||
mBatteryRefreshRate = Properties.getValue("battery_level_refresh_rate");
|
||||
|
||||
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)
|
||||
if (mIsApp) {
|
||||
if (mIsBatteryLevelEnabled and mHasService) {
|
||||
if (getWebhookId().equals("")) {
|
||||
if (mHasService) {
|
||||
mWebhookManager = new WebhookManager();
|
||||
if (getWebhookId().equals("")) {
|
||||
// System.println("Settings update(): Doing full webhook & sensor creation.");
|
||||
mWebhookManager.requestWebhookId();
|
||||
} else {
|
||||
// System.println("Settings update(): Doing just sensor creation.");
|
||||
// We already have a Webhook ID, so just enable or disable the sensor in Home Assistant.
|
||||
// Its a multiple step process, hence starting at step 0.
|
||||
mWebhookManager.registerWebhookSensor({
|
||||
"device_class" => "battery",
|
||||
"name" => "Battery Level",
|
||||
"state" => System.getSystemStats().battery,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "battery_level",
|
||||
"unit_of_measurement" => "%",
|
||||
"state_class" => "measurement",
|
||||
"entity_category" => "diagnostic",
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
}, 0);
|
||||
}
|
||||
if (mIsSensorsLevelEnabled) {
|
||||
// Create the timed activity
|
||||
if ((Background.getTemporalEventRegisteredTime() == null) or
|
||||
(Background.getTemporalEventRegisteredTime() != (mBatteryRefreshRate * 60))) {
|
||||
Background.registerForTemporalEvent(new Time.Duration(mBatteryRefreshRate * 60)); // Convert to seconds
|
||||
Background.registerForActivityCompletedEvent();
|
||||
}
|
||||
} else if (Background.getTemporalEventRegisteredTime() != null) {
|
||||
Background.deleteTemporalEvent();
|
||||
Background.deleteActivityCompletedEvent();
|
||||
}
|
||||
} else {
|
||||
// Explicitly disable the background event which persists when the application closes.
|
||||
// If !mHasService disable the Settings option as user feedback
|
||||
unsetIsBatteryLevelEnabled();
|
||||
unsetIsSensorsLevelEnabled();
|
||||
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
|
||||
}
|
||||
|
||||
static function unsetIsBatteryLevelEnabled() {
|
||||
mIsBatteryLevelEnabled = false;
|
||||
Properties.setValue("enable_battery_level", mIsBatteryLevelEnabled);
|
||||
static function isSensorsLevelEnabled() as Lang.Boolean {
|
||||
return mIsSensorsLevelEnabled;
|
||||
}
|
||||
|
||||
static function unsetIsSensorsLevelEnabled() {
|
||||
mIsSensorsLevelEnabled = false;
|
||||
Properties.setValue("enable_battery_level", mIsSensorsLevelEnabled);
|
||||
if (mHasService and (Background.getTemporalEventRegisteredTime() != null)) {
|
||||
Background.deleteTemporalEvent();
|
||||
Background.deleteActivityCompletedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
using Toybox.Lang;
|
||||
using Toybox.Communications;
|
||||
using Toybox.System;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
// Can use push view so must never be run in a glance context
|
||||
class WebhookManager {
|
||||
@@ -52,13 +53,13 @@ class WebhookManager {
|
||||
break;
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// 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);
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// 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);
|
||||
break;
|
||||
|
||||
@@ -77,25 +78,25 @@ class WebhookManager {
|
||||
"unit_of_measurement" => "%",
|
||||
"state_class" => "measurement",
|
||||
"entity_category" => "diagnostic",
|
||||
"disabled" => false
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
}, 0);
|
||||
} else {
|
||||
// System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
|
||||
Settings.unsetIsBatteryLevelEnabled();
|
||||
Settings.unsetIsSensorsLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
function requestWebhookId() {
|
||||
// System.println("WebhookManager requestWebhookId(): Requesting webhook id");
|
||||
var deviceSettings = System.getDeviceSettings();
|
||||
// System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
|
||||
Communications.makeWebRequest(
|
||||
Settings.getApiUrl() + "/mobile_app/registrations",
|
||||
{
|
||||
@@ -153,21 +154,24 @@ class WebhookManager {
|
||||
|
||||
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
|
||||
// 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.unsetIsBatteryLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
|
||||
requestWebhookId();
|
||||
break;
|
||||
|
||||
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.unsetIsBatteryLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
|
||||
requestWebhookId();
|
||||
break;
|
||||
|
||||
case 200:
|
||||
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");
|
||||
switch (step) {
|
||||
case 0:
|
||||
@@ -179,7 +183,7 @@ class WebhookManager {
|
||||
"type" => "binary_sensor",
|
||||
"unique_id" => "battery_is_charging",
|
||||
"entity_category" => "diagnostic",
|
||||
"disabled" => false
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
}, 1);
|
||||
break;
|
||||
case 1:
|
||||
@@ -198,12 +202,12 @@ class WebhookManager {
|
||||
"state" => activity,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "activity",
|
||||
"disabled" => false
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
}, 2);
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Activity");
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering next sensor: Sub-Activity");
|
||||
if (Activity has :getProfileInfo) {
|
||||
var sub_activity = Activity.getProfileInfo().subSport;
|
||||
if ((Activity.getActivityInfo() != null) and
|
||||
@@ -218,16 +222,18 @@ class WebhookManager {
|
||||
"state" => sub_activity,
|
||||
"type" => "sensor",
|
||||
"unique_id" => "sub_activity",
|
||||
"disabled" => false
|
||||
"disabled" => !Settings.isSensorsLevelEnabled()
|
||||
}, 3);
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
getApp().startUpdates();
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
// System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure");
|
||||
Settings.unsetWebhookId();
|
||||
Settings.unsetIsBatteryLevelEnabled();
|
||||
Settings.unsetIsSensorsLevelEnabled();
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String);
|
||||
}
|
||||
break;
|
||||
@@ -235,15 +241,18 @@ class WebhookManager {
|
||||
default:
|
||||
// System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(): URL=" + url);
|
||||
// https://developers.home-assistant.io/docs/api/native-app-integration/sensors/#registering-a-sensor
|
||||
Communications.makeWebRequest(
|
||||
Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId(),
|
||||
url,
|
||||
{
|
||||
"type" => "register_sensor",
|
||||
"data" => sensor
|
||||
|
||||
Reference in New Issue
Block a user