Skip to content

Commit 63bc127

Browse files
authored
Perform all updates in one request (#173)
2 parents 0feecde + af43fde commit 63bc127

10 files changed

+240
-469
lines changed

resources/settings/properties.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
this delayfor an "always open" mode of operation, which then drains the
5757
watch battery from the additional API access activity.
5858
-->
59-
<property id="poll_delay" type="number">0</property>
59+
<property id="poll_delay_combined" type="number">5</property>
6060

6161
<!--
6262
After this time (in seconds), a confirmation dialog for an action is

resources/settings/settings.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
</setting>
6767

6868
<setting
69-
propertyKey="@Properties.poll_delay"
69+
propertyKey="@Properties.poll_delay_combined"
7070
title="@Strings.SettingsPollDelay"
7171
>
7272
<settingConfig type="numeric" min="0" />

source/ErrorView.mc

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

source/HomeAssistantApp.mc

+119-38
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,8 @@ class HomeAssistantApp extends Application.AppBase {
3535
private var mUpdateTimer as Timer.Timer or Null;
3636
// Array initialised by onReturnFetchMenuConfig()
3737
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTemplateMenuItem> or Null;
38-
private var mNextItemToUpdate as Lang.Number = 0; // Index into the above array
3938
private var mIsGlance as Lang.Boolean = false;
4039
private var mIsApp as Lang.Boolean = false; // Or Widget
41-
private var mIsInitUpdateCompl as Lang.Boolean = false;
4240
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
4341

4442
function initialize() {
@@ -262,15 +260,129 @@ class HomeAssistantApp extends Application.AppBase {
262260
mQuitTimer.begin();
263261
}
264262

263+
var mTemplates as Lang.Dictionary = {};
265264
function startUpdates() {
266265
if (mHaMenu != null and !mUpdating) {
267266
mItemsToUpdate = mHaMenu.getItemsToUpdate();
268267
// Start the continuous update process that continues for as long as the application is running.
269-
// The chain of functions from 'updateNextMenuItem()' calls 'updateNextMenuItem()' on completion.
270-
if (mItemsToUpdate.size() > 0) {
271-
mUpdating = true;
272-
updateNextMenuItemInternal();
268+
mTemplates = {};
269+
for (var i = 0; i < mItemsToUpdate.size(); i++) {
270+
var item = mItemsToUpdate[i];
271+
var template = item.buildTemplate();
272+
if (template != null) {
273+
mTemplates.put(i.toString(), {
274+
"template" => template
275+
});
276+
}
277+
if (item instanceof HomeAssistantToggleMenuItem) {
278+
mTemplates.put(i.toString() + "t", {
279+
"template" => (item as HomeAssistantToggleMenuItem).buildToggleTemplate()
280+
});
281+
}
273282
}
283+
updateMenuItems();
284+
}
285+
}
286+
287+
function onReturnUpdateMenuItems(responseCode as Lang.Number, data as Null or Lang.Dictionary) as Void {
288+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: " + responseCode);
289+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Data: " + data);
290+
291+
var status = WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String;
292+
switch (responseCode) {
293+
case Communications.BLE_HOST_TIMEOUT:
294+
case Communications.BLE_CONNECTION_UNAVAILABLE:
295+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
296+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
297+
break;
298+
299+
case Communications.BLE_QUEUE_FULL:
300+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
301+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
302+
break;
303+
304+
case Communications.NETWORK_REQUEST_TIMED_OUT:
305+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
306+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
307+
break;
308+
309+
case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
310+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
311+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
312+
break;
313+
314+
case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
315+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
316+
var myTimer = new Timer.Timer();
317+
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
318+
myTimer.start(method(:updateMenuItems), Globals.scApiBackoff, false);
319+
// Revert status
320+
status = getApiStatus();
321+
break;
322+
323+
case 404:
324+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 404, page not found. Check API URL setting.");
325+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
326+
break;
327+
328+
case 400:
329+
// System.println("HomeAssistantApp onReturnUpdateMenuItems() Response Code: 400, bad request. Template error.");
330+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
331+
break;
332+
333+
case 200:
334+
status = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
335+
for (var i = 0; i < mItemsToUpdate.size(); i++) {
336+
var item = mItemsToUpdate[i];
337+
var state = data.get(i.toString());
338+
item.updateState(state);
339+
if (item instanceof HomeAssistantToggleMenuItem) {
340+
(item as HomeAssistantToggleMenuItem).updateToggleState(data.get(i.toString() + "t"));
341+
}
342+
}
343+
var delay = Settings.getPollDelay();
344+
if (delay > 0) {
345+
mUpdateTimer.start(method(:updateMenuItems), delay, false);
346+
} else {
347+
updateMenuItems();
348+
}
349+
break;
350+
351+
default:
352+
// System.println("HomeAssistantApp onReturnUpdateMenuItems(): Unhandled HTTP response code = " + responseCode);
353+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
354+
}
355+
setApiStatus(status);
356+
}
357+
358+
function updateMenuItems() as Void {
359+
if (! System.getDeviceSettings().phoneConnected) {
360+
// System.println("HomeAssistantApp updateMenuItems(): No Phone connection, skipping API call.");
361+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String + ".");
362+
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
363+
} else if (! System.getDeviceSettings().connectionAvailable) {
364+
// System.println("HomeAssistantApp updateMenuItems(): No Internet connection, skipping API call.");
365+
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String + ".");
366+
setApiStatus(WatchUi.loadResource($.Rez.Strings.Unavailable) as Lang.String);
367+
} else {
368+
// https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates
369+
var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
370+
// System.println("HomeAssistantApp updateMenuItems() URL=" + url + ", Template='" + mTemplate + "'");
371+
Communications.makeWebRequest(
372+
url,
373+
{
374+
"type" => "render_template",
375+
"data" => mTemplates
376+
},
377+
{
378+
:method => Communications.HTTP_REQUEST_METHOD_POST,
379+
:headers => {
380+
"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
381+
},
382+
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
383+
},
384+
method(:onReturnUpdateMenuItems)
385+
);
274386
}
275387
}
276388

@@ -403,45 +515,14 @@ class HomeAssistantApp extends Application.AppBase {
403515
WatchUi.pushView(mHaMenu, new HomeAssistantViewDelegate(true), WatchUi.SLIDE_IMMEDIATE);
404516
}
405517

406-
function updateNextMenuItem() as Void {
407-
var delay = Settings.getPollDelay();
408-
if (mIsInitUpdateCompl and (delay > 0) and (mNextItemToUpdate == 0)) {
409-
mUpdateTimer.start(method(:updateNextMenuItemInternal), delay, false);
410-
} else {
411-
updateNextMenuItemInternal();
412-
}
413-
}
414-
415518
// Only call this function if Settings.getPollDelay() > 0. This must be tested locally as it is then efficient to take
416519
// alternative action if the test fails.
417520
function forceStatusUpdates() as Void {
418521
// Don't mess with updates unless we are using a timer.
419522
if (Settings.getPollDelay() > 0) {
420523
mUpdateTimer.stop();
421-
mIsInitUpdateCompl = false;
422-
// Start from the beginning, or we will only get a partial round of updates before mIsInitUpdateCompl is flipped.
423-
mNextItemToUpdate = 0;
424524
// For immediate updates
425-
updateNextMenuItem();
426-
}
427-
}
428-
429-
// We need to spread out the API calls so as not to overload the results queue and cause Communications.BLE_QUEUE_FULL
430-
// (-101) error. This function is called by a timer every Globals.menuItemUpdateInterval ms.
431-
function updateNextMenuItemInternal() as Void {
432-
if (mItemsToUpdate != null) {
433-
// System.println("HomeAssistantApp updateNextMenuItemInternal(): Doing update for item " + mNextItemToUpdate + ", mIsInitUpdateCompl=" + mIsInitUpdateCompl);
434-
mItemsToUpdate[mNextItemToUpdate].getState();
435-
// mNextItemToUpdate = (mNextItemToUpdate + 1) % mItemsToUpdate.size() - But with roll-over detection
436-
if (mNextItemToUpdate == mItemsToUpdate.size()-1) {
437-
// Last item completed return to the start of the list
438-
mNextItemToUpdate = 0;
439-
mIsInitUpdateCompl = true;
440-
} else {
441-
mNextItemToUpdate++;
442-
}
443-
// } else {
444-
// System.println("HomeAssistantApp updateNextMenuItemInternal(): No menu items to update");
525+
updateMenuItems();
445526
}
446527
}
447528

source/HomeAssistantGroupMenuItem.mc

+33-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
using Toybox.Lang;
2323
using Toybox.WatchUi;
2424

25-
class HomeAssistantGroupMenuItem extends TemplateMenuItem {
25+
class HomeAssistantGroupMenuItem extends WatchUi.IconMenuItem {
26+
private var mTemplate as Lang.String or Null;
2627
private var mMenu as HomeAssistantView;
2728

2829
function initialize(
@@ -34,20 +35,47 @@ class HomeAssistantGroupMenuItem extends TemplateMenuItem {
3435
} or Null
3536
) {
3637

37-
TemplateMenuItem.initialize(
38+
WatchUi.IconMenuItem.initialize(
3839
definition.get("name") as Lang.String,
39-
template,
40-
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
41-
getApp().method(:updateNextMenuItem),
40+
null,
41+
null,
4242
icon,
4343
options
4444
);
4545

46+
mTemplate = template;
4647
mMenu = new HomeAssistantView(definition, null);
4748
}
4849

50+
function buildTemplate() as Lang.String or Null {
51+
return mTemplate;
52+
}
53+
54+
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
55+
if (data == null) {
56+
setSubLabel(null);
57+
} else if(data instanceof Lang.String) {
58+
setSubLabel(data);
59+
} else if(data instanceof Lang.Dictionary) {
60+
// System.println("HomeAsistantGroupMenuItem updateState() data = " + data);
61+
if (data.get("error") != null) {
62+
setSubLabel($.Rez.Strings.TemplateError);
63+
} else {
64+
setSubLabel($.Rez.Strings.PotentialError);
65+
}
66+
} else {
67+
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
68+
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
69+
}
70+
WatchUi.requestUpdate();
71+
}
72+
4973
function getMenuView() as HomeAssistantView {
5074
return mMenu;
5175
}
5276

77+
function hasTemplate() as Lang.Boolean {
78+
return mTemplate != null;
79+
}
80+
5381
}

source/HomeAssistantTapMenuItem.mc

+7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class HomeAssistantTapMenuItem extends WatchUi.IconMenuItem {
5353
mData = data;
5454
}
5555

56+
function buildTemplate() as Lang.String or Null {
57+
return null;
58+
}
59+
60+
function updateState(data as Lang.String or Null) as Void {
61+
}
62+
5663
function callService() as Void {
5764
if (mConfirm) {
5865
WatchUi.pushView(

source/HomeAssistantTemplateMenuItem.mc

+29-5
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ using Toybox.Lang;
2626
using Toybox.WatchUi;
2727
using Toybox.Graphics;
2828

29-
class HomeAssistantTemplateMenuItem extends TemplateMenuItem {
29+
class HomeAssistantTemplateMenuItem extends WatchUi.IconMenuItem {
3030
private var mHomeAssistantService as HomeAssistantService;
31+
private var mTemplate as Lang.String;
3132
private var mService as Lang.String or Null;
3233
private var mConfirm as Lang.Boolean;
3334
private var mData as Lang.Dictionary or Null;
@@ -44,21 +45,44 @@ class HomeAssistantTemplateMenuItem extends TemplateMenuItem {
4445
} or Null,
4546
haService as HomeAssistantService
4647
) {
47-
TemplateMenuItem.initialize(
48+
WatchUi.IconMenuItem.initialize(
4849
label,
49-
template,
50-
// Now this feels very "closely coupled" to the application, but it is the most reliable method instead of using a timer.
51-
getApp().method(:updateNextMenuItem),
50+
null,
51+
null,
5252
icon,
5353
options
5454
);
5555

5656
mHomeAssistantService = haService;
57+
mTemplate = template;
5758
mService = service;
5859
mConfirm = confirm;
5960
mData = data;
6061
}
6162

63+
function buildTemplate() as Lang.String or Null {
64+
return mTemplate;
65+
}
66+
67+
function updateState(data as Lang.String or Lang.Dictionary or Null) as Void {
68+
if (data == null) {
69+
setSubLabel($.Rez.Strings.Empty);
70+
} else if(data instanceof Lang.String) {
71+
setSubLabel(data);
72+
} else if(data instanceof Lang.Dictionary) {
73+
// System.println("HomeAsistantTemplateMenuItem updateState() data = " + data);
74+
if (data.get("error") != null) {
75+
setSubLabel($.Rez.Strings.TemplateError);
76+
} else {
77+
setSubLabel($.Rez.Strings.PotentialError);
78+
}
79+
} else {
80+
// The template must return a Lang.String, a number can be either integer or float and hence cannot be formatted locally without error.
81+
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
82+
}
83+
WatchUi.requestUpdate();
84+
}
85+
6286
function callService() as Void {
6387
if (mConfirm) {
6488
WatchUi.pushView(

0 commit comments

Comments
 (0)