Perform all updates in one request (#173)

This commit is contained in:
Philip Abbey
2024-08-29 20:55:40 +01:00
committed by GitHub
10 changed files with 241 additions and 470 deletions

View File

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

View File

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

View File

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

View File

@ -35,10 +35,8 @@ class HomeAssistantApp extends Application.AppBase {
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 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;
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
function initialize() { function initialize() {
@ -262,15 +260,129 @@ class HomeAssistantApp extends Application.AppBase {
mQuitTimer.begin(); mQuitTimer.begin();
} }
var mTemplates as Lang.Dictionary = {};
function startUpdates() { function startUpdates() {
if (mHaMenu != null and !mUpdating) { if (mHaMenu != null and !mUpdating) {
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. mTemplates = {};
if (mItemsToUpdate.size() > 0) { for (var i = 0; i < mItemsToUpdate.size(); i++) {
mUpdating = true; var item = mItemsToUpdate[i];
updateNextMenuItemInternal(); var template = item.buildTemplate();
if (template != null) {
mTemplates.put(i.toString(), {
"template" => template
});
}
if (item instanceof HomeAssistantToggleMenuItem) {
mTemplates.put(i.toString() + "t", {
"template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate()
});
}
} }
updateMenuItems();
}
}
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(method(:updateMenuItems), Globals.scApiBackoff, false);
// Revert status
status = getApiStatus();
break;
case 404:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
case 400:
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 400, bad request. Template error.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
for (var i = 0; i < mItemsToUpdate.size(); i++) {
var item = mItemsToUpdate[i];
var state = data.get(i.toString());
item.updateState(state);
if (item instanceof HomeAssistantToggleMenuItem) {
(item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t"));
}
}
var delay = Settings.getPollDelay();
if (delay > 0) {
mUpdateTimer.start(method(:updateMenuItems), delay, false);
} else {
updateMenuItems();
}
break;
default:
// System.println("HomeAssistantApp onReturnUpdateMenuItems(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
setApiStatus(status);
}
function updateMenuItems() as Void {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else {
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
url,
{
"type" => "render_template",
"data" => mTemplates
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnUpdateMenuItems)
);
} }
} }
@ -403,45 +515,14 @@ class HomeAssistantApp extends Application.AppBase {
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE); WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
} }
function updateNextMenuItem() as Void {
var delay = Settings.getPollDelay();
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
} else {
updateNextMenuItemInternal();
}
}
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take // 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. // alternative action if the test fails.
function forceStatusUpdates() as Void { function forceStatusUpdates() as Void {
// Don't mess with updates unless we are using a timer. // Don't mess with updates unless we are using a timer.
if (Settings.getPollDelay() > 0) { if (Settings.getPollDelay() > 0) {
mUpdateTimer.stop(); 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 // For immediate updates
updateNextMenuItem(); updateMenuItems();
}
}
// 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 {
if (mItemsToUpdate != null) {
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
mItemsToUpdate[mNextItemToUpdate].getState();
// mNextItemToUpdate = (mNextItemToUpdate + 1) % mItemsToUpdate.size() - But with roll-over detection
if (mNextItemToUpdate == mItemsToUpdate.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");
} }
} }

View File

@ -22,7 +22,8 @@
using Toybox.Lang; using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
class HomeAssistantGroupMenuItem extends TemplateMenuItem { class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
private var mTemplate as Lang.String or Null;
private var mMenu as HomeAssistantView; private var mMenu as HomeAssistantView;
function initialize( function initialize(
@ -34,20 +35,47 @@ class HomeAssistantGroupMenuItem extends TemplateMenuItem {
} or Null } or Null
) { ) {
TemplateMenuItem.initialize( WatchUi.IconMenuItem.initialize(
definition.get("name") as Lang.String, definition.get("name") as Lang.String,
template, null,
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. null,
getApp().method(:updateNextMenuItem),
icon, icon,
options options
); );
mTemplate = template;
mMenu = new HomeAssistantView(definition, null); mMenu = new HomeAssistantView(definition, null);
} }
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel(null);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantGroupMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function getMenuView() as HomeAssistantView { function getMenuView() as HomeAssistantView {
return mMenu; return mMenu;
} }
function hasTemplate() as Lang.Boolean {
return mTemplate != null;
}
} }

View File

@ -53,6 +53,13 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
mData = data; mData = data;
} }
function buildTemplate() as Lang.String or Null {
return null;
}
function updateState(data as Lang.String or Null) as Void {
}
function callService() as Void { function callService() as Void {
if (mConfirm) { if (mConfirm) {
WatchUi.pushView( WatchUi.pushView(

View File

@ -26,8 +26,9 @@ using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
class HomeAssistantTemplateMenuItem extends TemplateMenuItem { class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
private var mHomeAssistantService as HomeAssistantService; private var mHomeAssistantService as HomeAssistantService;
private var mTemplate as Lang.String;
private var mService as Lang.String or Null; private var mService as Lang.String or Null;
private var mConfirm as Lang.Boolean; private var mConfirm as Lang.Boolean;
private var mData as Lang.Dictionary or Null; private var mData as Lang.Dictionary or Null;
@ -44,21 +45,44 @@ class HomeAssistantTemplateMenuItem extends TemplateMenuItem {
} or Null, } or Null,
haService as HomeAssistantService haService as HomeAssistantService
) { ) {
TemplateMenuItem.initialize( WatchUi.IconMenuItem.initialize(
label, label,
template, null,
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer. null,
getApp().method(:updateNextMenuItem),
icon, icon,
options options
); );
mHomeAssistantService = haService; mHomeAssistantService = haService;
mTemplate = template;
mService = service; mService = service;
mConfirm = confirm; mConfirm = confirm;
mData = data; mData = data;
} }
function buildTemplate() as Lang.String or Null {
return mTemplate;
}
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(data instanceof Lang.String) {
setSubLabel(data);
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
WatchUi.requestUpdate();
}
function callService() as Void { function callService() as Void {
if (mConfirm) { if (mConfirm) {
WatchUi.pushView( WatchUi.pushView(

View File

@ -49,125 +49,61 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
if (state != null) { if (state != null) {
if (state.equals("on") && !isEnabled()) { if (state.equals("on") && !isEnabled()) {
setEnabled(true); setEnabled(true);
WatchUi.requestUpdate();
} else if (state.equals("off") && isEnabled()) { } else if (state.equals("off") && isEnabled()) {
setEnabled(false); setEnabled(false);
WatchUi.requestUpdate();
} }
} }
} }
// Callback function after completing the GET request to fetch the status. function buildTemplate() as Lang.String or Null {
// Terminate updating the toggle menu items via the chain of calls for a permanent network return mTemplate;
// error. The ErrorView cancellation will resume the call chain. }
// function buildToggleTemplate() as Lang.String or Null {
function onReturnGetState(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void { return "{{states('" + mData.get("entity_id") + "')}}";
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: " + responseCode);
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Abandon the update to this menu item, and any template, and move on to the next with a back-off delay.
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break;
case 404:
var msg = null;
if (data != null) {
msg = data.get("message");
}
if (msg != null) {
// Should be an HTTP 404 according to curl queries
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404. " + mData.get("entity_id") + " " + msg);
ErrorView.show("HTTP 404, " + mData.get("entity_id") + ". " + data.get("message"));
} else {
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
}
break;
case 405:
// System.println("HomeAssistantToggleMenuItem onReturnGetState() Response Code: 405. " + mData.get("entity_id") + " " + data.get("message"));
ErrorView.show("HTTP 405, " + mData.get("entity_id") + ". " + data.get("message"));
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
var state = data.get("state") as Lang.String;
// System.println((data.get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
if (getLabel().equals("...")) {
setLabel((data.get("attributes") as Lang.Dictionary).get("friendly_name") as Lang.String);
}
setUiToggle(state);
if (mTemplate == null) {
// Nothing more to do
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
} else {
updateTemplate();
}
break;
default:
// System.println("HomeAssistantToggleMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
getApp().setApiStatus(status);
} }
function getState() as Void { function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (! System.getDeviceSettings().phoneConnected) { if (data == null) {
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call."); setSubLabel(null);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + "."); } else if(data instanceof Lang.String) {
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); setSubLabel(data);
} else if (! System.getDeviceSettings().connectionAvailable) { } else if(data instanceof Lang.Dictionary) {
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call."); // System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + "."); if (data.get("error") != null) {
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String); setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else { } else {
var url = Settings.getApiUrl() + "/states/" + mData.get("entity_id"); // The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
// System.println("HomeAssistantToggleMenuItem getState() URL=" + url); setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
Communications.makeWebRequest(
url,
null,
{
:method => Communications.HTTP_REQUEST_METHOD_GET,
:headers => {
"Authorization" => "Bearer " + Settings.getApiKey()
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnGetState)
);
} }
WatchUi.requestUpdate();
}
function updateToggleState(data as Lang.String or Lang.Dictionary or Null) as Void {
if (data == null) {
setUiToggle("off");
} else if(data instanceof Lang.String) {
setUiToggle(data);
if (mTemplate == null and data.equals("unavailable")) {
setSubLabel($.Rez.Strings.Unavailable);
}
} else if(data instanceof Lang.Dictionary) {
// System.println("HomeAsistantToggleMenuItem updateState() data = " + data);
if (mTemplate == null) {
if (data.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
if (mTemplate == null) {
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
}
WatchUi.requestUpdate();
} }
// Callback function after completing the POST request to set the status. // Callback function after completing the POST request to set the status.
@ -214,6 +150,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
state = d[i].get("state") as Lang.String; state = d[i].get("state") as Lang.String;
// System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state); // System.println((d[i].get("attributes") as Lang.Dictionary).get("friendly_name") + " State=" + state);
setUiToggle(state); setUiToggle(state);
WatchUi.requestUpdate();
} }
} }
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String; status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
@ -227,15 +164,14 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
} }
function setState(s as Lang.Boolean) as Void { function setState(s as Lang.Boolean) as Void {
// Toggle the UI back, we'll wait for confirmation from the Home Assistant
setEnabled(!isEnabled());
if (! System.getDeviceSettings().phoneConnected) { if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call."); // System.println("HomeAssistantToggleMenuItem getState(): No Phone connection, skipping API call.");
// Toggle the UI back
setEnabled(!isEnabled());
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + "."); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
} else if (! System.getDeviceSettings().connectionAvailable) { } else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call."); // System.println("HomeAssistantToggleMenuItem getState(): No Internet connection, skipping API call.");
// Toggle the UI back // Toggle the UI back
setEnabled(!isEnabled());
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + "."); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
} else { } else {
// Updated SDK and got a new error // Updated SDK and got a new error
@ -281,127 +217,4 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
setState(b); setState(b);
} }
// Callback function after completing the GET request to fetch the status.
// Terminate updating the toggle menu items via the chain of calls for a permanent network
// error. The ErrorView cancellation will resume the call chain.
//
function onReturnUpdateTemplate(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: " + responseCode);
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Data: " + data);
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
switch (responseCode) {
case Communications.BLE_HOST_TIMEOUT:
case Communications.BLE_CONNECTION_UNAVAILABLE:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
break;
case Communications.BLE_QUEUE_FULL:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
break;
case Communications.NETWORK_REQUEST_TIMED_OUT:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
break;
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
break;
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
var myTimer = new Timer.Timer();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
myTimer.start(getApp().method(:updateNextMenuItem), Globals.scApiBackoff, false);
// Revert status
status = getApp().getApiStatus();
break;
case 404:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 404, page not found. Check API URL setting.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
break;
case 400:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() Response Code: 400, bad request. Template error.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
break;
case 200:
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
var label = data.get("request");
if (label == null) {
setSubLabel($.Rez.Strings.Empty);
} else if(label instanceof Lang.String) {
setSubLabel(label);
} else if(label instanceof Lang.Dictionary) {
// System.println("HomeAssistantTemplateMenuItem onReturnGetState() label = " + label);
if (label.get("error") != null) {
setSubLabel($.Rez.Strings.TemplateError);
} else {
setSubLabel($.Rez.Strings.PotentialError);
}
} else {
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
}
requestUpdate();
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
break;
default:
// System.println("HomeAssistantTemplateMenuItem onReturnGetState(): Unhandled HTTP response code = " + responseCode);
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
}
getApp().setApiStatus(status);
}
// Massive code duplication from TemplateMenuItem, but cannot inherit from two classes.
//
function updateTemplate() as Void {
if (mTemplate == null) {
// Nothing to do here.
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
getApp().updateNextMenuItem();
} else {
if (! System.getDeviceSettings().phoneConnected) {
// System.println("HomeAssistantTemplateMenuItem getState(): No Phone connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else if (! System.getDeviceSettings().connectionAvailable) {
// System.println("HomeAssistantTemplateMenuItem getState(): No Internet connection, skipping API call.");
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
getApp().setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
} else {
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
// System.println("HomeAssistantTemplateMenuItem getState() URL=" + url + ", Template='" + mTemplate + "'");
Communications.makeWebRequest(
url,
{
"type" => "render_template",
"data" => {
"request" => {
"template" => mTemplate
}
}
},
{
:method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => {
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
},
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
},
method(:onReturnUpdateTemplate)
);
}
}
}
} }

View File

@ -57,7 +57,7 @@ class Settings {
mClearCache = Properties.getValue("clear_cache"); mClearCache = Properties.getValue("clear_cache");
mVibrate = Properties.getValue("enable_vibration"); mVibrate = Properties.getValue("enable_vibration");
mAppTimeout = Properties.getValue("app_timeout"); mAppTimeout = Properties.getValue("app_timeout");
mPollDelay = Properties.getValue("poll_delay"); mPollDelay = Properties.getValue("poll_delay_combined");
mConfirmTimeout = Properties.getValue("confirm_timeout"); mConfirmTimeout = Properties.getValue("confirm_timeout");
mMenuAlignment = Properties.getValue("menu_alignment"); mMenuAlignment = Properties.getValue("menu_alignment");
mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level"); mIsSensorsLevelEnabled = Properties.getValue("enable_battery_level");

View File

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