misc/watchy-experimentation#5: Make HTTP requests less frequently



Issue Information

Issue Type: issue
Status: closed
Reported By: btasker
Assigned To: btasker

Created: 16-Jun-23 07:59



Description

Issue #4 adjusted the watch face so that we fetch and display solar efficiency data.

Although this works, the watch's battery life isn't great (makes sense, we're doing SSL stuff every minute).

So I want to adjust so that we only fetch/write periodically.

Currently we:

  • On odd minutes, fetch solar efficiency data
  • On even minutes, display temperature
  • Every minute write stats (#3) to InfluxDB

What I want to adjust to is

  • On even minutes, display temperature
  • On odd minutes, display a cached solar efficiency value
  • Every 5 minutes update the solar efficiency value and cache
  • Every 5 minutes, write stats to InfluxDB

To make creating the 5 minute interval simpler, solar/temperature have been swapped between odd and even.



Issue Links

Toggle State Changes

Activity


assigned to @btasker

marked this issue as related to #4

marked this issue as related to #3

mentioned in issue #4

verified

mentioned in commit sysconfigs/watchy-code@76deec75dfd0875b128f1d9c6e54f7ccccfeff1c

Commit: sysconfigs/watchy-code@76deec75dfd0875b128f1d9c6e54f7ccccfeff1c 
Author: B Tasker                            
                            
Date: 2023-06-16T09:07:06.000+01:00 

Message

Move to only polling solar status every 5 mins and cache result (misc/watchy-experimentation#5)

Out of (developer) convenience, this also swaps when solar efficiency data displays to odd minutes. Weather temperature data will now display on even minutes only.

+76 -43 (119 lines changed)
verified

mentioned in commit sysconfigs/watchy-code@4aa008383bd3b59d57044359b293061aec5a8ac3

Commit: sysconfigs/watchy-code@4aa008383bd3b59d57044359b293061aec5a8ac3 
Author: B Tasker                            
                            
Date: 2023-06-16T11:17:11.000+01:00 

Message

Switch distribution between even+odd minutes back (misc/watchy-experimentation#5)

+4 -4 (8 lines changed)
verified

mentioned in commit sysconfigs/watchy-code@2ab8bd93a963a6051934a1b7fcdbf6323d60543b

Commit: sysconfigs/watchy-code@2ab8bd93a963a6051934a1b7fcdbf6323d60543b 
Author: B Tasker                            
                            
Date: 2023-06-16T09:22:40.000+01:00 

Message

Switch cache storage type (misc/watchy-experimentation#5)

Moves from RTC_NOINIT_ATTR to RTC_DATA_ATTR.

Haven't yet looked into why this makes a difference, but it seems to

+5 -4 (9 lines changed)
verified

mentioned in commit sysconfigs/watchy-code@76bb5520f079d956b8cd1532f83e0786140a9edd

Commit: sysconfigs/watchy-code@76bb5520f079d956b8cd1532f83e0786140a9edd 
Author: B Tasker                            
                            
Date: 2023-06-16T11:11:22.000+01:00 

Message

Revert "Switch cache storage type (misc/watchy-experimentation#5)"

This reverts commit 2ab8bd93a963a6051934a1b7fcdbf6323d60543b.

+4 -5 (9 lines changed)

I've run into an issue storing the cached solar efficiency data with RTC_NOINIT_ATTR.

Rather than getting the written value back, we get a long number (currently it seems to consistently be 786411844).

It looks like this might be because RTC_SLOW_MEM isn't being powered during deep sleep. That forum post says it's a bug, but I wondered if it was also possible that Watchy is specifically disabling it:

ben@optimus:~/Arduino$ grep -Rl esp_sleep_pd_config *
libraries/Watchy/examples/WatchFaces/7_SEG/Watchy_7_SEG.cpp

Seems not.

I also wondered if I was relying on a false assumption - is the issue with that variable or are we receiving bad data from upstream?

Adding a couple of display.print() lines suggests upstream auth has started failing, so we're not actually getting a result back to write into that variable - we're probably just seeing the initial value.

Switched auth and it suddenly appears to be working just fine.

You have to actually retrieve data to be able to use it.... who knew? :)

verified

mentioned in commit sysconfigs/watchy-code@162015cf15daa5ea2e6e5eecc54cb6e4ab3dc3e3

Commit: sysconfigs/watchy-code@162015cf15daa5ea2e6e5eecc54cb6e4ab3dc3e3 
Author: B Tasker                            
                            
Date: 2023-06-16T12:06:00.000+01:00 

Message

Fix auth - failing auth turned out to be the cause of issues in misc/watchy-experimentation#5

+5 -4 (9 lines changed)

Currently, we're still toggling the Wifi on and off once a minute, because that happens in the calling function:

    if (connectWiFi()) {
        // Trigger solar state update
        getSolarState();

        // Push stats to InfluxDB
        writeStats();

        // Turn the radios back off
        WiFi.mode(WIFI_OFF); 
        btStop();    
    }

I think it's probably within the spirit of this issue to adjust that too.

We trigger (non weather) actions if the following is true:

  • Stats write currentTime.Minute % 5 == 0
  • Solar display currentTime.Minute % 2 != 0
  • Solar value fetch currentTime.Minute % 5 == 0

We don't actually need the wifi on for solar display either.

So, it might be better to break that calling wrapper up a bit

    if (currentTime.Minute % 5 == 0 && connectWiFi()) {
        // Trigger solar state update
        getSolarState();

        // Push stats to InfluxDB
        writeStats();

        // Turn the radios back off
        WiFi.mode(WIFI_OFF); 
        btStop();    
    } else {
        getSolarState();
    }

It's not very tidy to read but can't easily be abstracted out: we can't just set (and pass) a boolean to a single call of getSolarState because we need to turn the radio off.

However, we probably can make getSolarState() a little more CPU efficient. As we've already calculated the current minute modulo 5 we could just pass a boolean into getSolarState() so that it doesn't have to repeat the operation.

We could, in fact, also pass the result of currentTime.Minute % 2 into both drawWeather and getSolarState - they both use them to identify which should be displaying for the current minute.

verified

mentioned in commit sysconfigs/watchy-code@e61409560f2e170b40a27860e1202f26f53d6331

Commit: sysconfigs/watchy-code@e61409560f2e170b40a27860e1202f26f53d6331 
Author: B Tasker                            
                            
Date: 2023-06-16T12:20:48.000+01:00 

Message

Only toggle wifi on if we're intended to place requests (misc/watchy-experimentation#5)

This gates the call to connectWiFi with a check of the current minute modulo 5.

+12 -5 (17 lines changed)

OK, so we now have the following update/display pattern

  1. Wifi turned on if currentTime.Minute % 5 == 0
  2. Fetch solar data if currentTime.Minute % 5 == 0
  3. Write stats if currentTime.Minute % 5 == 0
  4. Display weather temperature if currentTime.Minute % 2 == 0
  5. Display solar data if currentTime.Minute % 2 != 0

The combination of 2. and 5. means that we don't actually fetch Solar data every 5 minutes, but instead every 10.

The reason for this is that getSolarState() will only be called if the current minute is odd. Minutes 00,10,20,30,40 and 50 are not odd, therefore it won't be called (and so won't trigger an update).

It wasn't entirely intentional, but I'm OK with this - I had been starting to think that a less frequent update cadence could be wise anyway, so this is a good result (as it's been achieved without wasting CPU time calculating another modulo).

To reduce CPU time, we now aim to calculate modulos once and pass a boolean in subsequent calls:

void Watchy7SEG::drawWatchFace(){

    bool minute_is_even = (currentTime.Minute % 2 == 0);

    display.fillScreen(DARKMODE ? GxEPD_BLACK : GxEPD_WHITE);
    display.setTextColor(DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    drawTime();
    drawDate();
    drawSteps();
    drawWeather(minute_is_even);
    drawBattery();
    display.drawBitmap(120, 77, WIFI_CONFIGURED ? wifi : wifioff, 26, 18, DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    if(BLE_CONFIGURED){
        display.drawBitmap(100, 75, bluetooth, 13, 21, DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    }

    if (currentTime.Minute % 5 == 0 && connectWiFi()) {
        // Trigger solar state update
        if (!minute_is_even){
          getSolarState(true);
        }

        // Push stats to InfluxDB
        writeStats();

        // Turn the radios back off
        WiFi.mode(WIFI_OFF); 
        btStop();    
    } else {
        if (!minute_is_even){
          getSolarState(false);
        }
    }
}

Re-opening to explore whether we're still polling too regularly

I had been meaning to look at adjusting how often calls are made to OpenWeatherAPI, but having looked into it they're actually already restricted to every 30 ticks (with each tick being a minute in this case).

In Watchy.cpp the relevant function starts as

weatherData Watchy::getWeatherData(String cityID, String units, String lang,
                                   String url, String apiKey,
                                   uint8_t updateInterval) {
  currentWeather.isMetric = units == String("metric");
  if (weatherIntervalCounter < 0) { //-1 on first run, set to updateInterval
    weatherIntervalCounter = updateInterval;
  }
  if (weatherIntervalCounter >=
      updateInterval) { // only update if WEATHER_UPDATE_INTERVAL has elapsed
                        // i.e. 30 minutes

So actually, the weather stuff only makes 2 external calls an hour.

By comparison, we've added

  • 12 stats write calls
  • 6 Solar state retrieval calls

Increasing the number of external connections/hour by 10x.

Probably explains why the battery lasts about a day, rather than days.

If we switched to a 15 minute interval, we'd have

  • 4 stats write calls
  • 4 Solar state retrieval calls

That's still a 5x increase (though it halves the number of requests vs now).

If we moved to 30 minute it'd be a 3x increase (there's no getting away from it being a sizeable increase - the base number is low, and we're doing a few things at once).

Obviously we could get it down to 1 by hosting a dedicated application, but I explicitly want to avoid that (at least for now: it could be interesting to put something together in LUA to handle it in OpenResty).

I think the answer is to move to 30 minutes and see how the battery life looks and then perhaps look at increasing.

I'll raise a seperate ticket if I decide to do it, but I have also considered having telegraf fetch the OpenWeatherAPI information and write into InfluxDB - that way the weather data could be fetched with a simple InfluxQL query, potentially cutting out some processing overhead vs using the API.

verified

mentioned in commit sysconfigs/watchy-code@4815c386a1dc7aa0948d8371df0d7506410c270a

Commit: sysconfigs/watchy-code@4815c386a1dc7aa0948d8371df0d7506410c270a 
Author: B Tasker                            
                            
Date: 2023-06-16T23:36:56.000+01:00 

Message

Move to making HTTP requests once every 30 minutes (misc/watchy-experimentation#5)

This amends our timing logic so that we update Solar efficiency data and write out stats once every 30 minutes.

The main aim here is to improve battery life

+1 -1 (2 lines changed)

One minor drawback with the recent change: just after startup (i.e. after a new image has been flashed, or the watch's battery ran out etc), the Solar data won't currently show anything (because it'll not have been retrieved yet).

That was an issue prior to the change, but only really persisted for 5-10 minutes at most.

Having it persist for 30 minutes seems a bit much - we could check the cache for an initialisation value and run a fetch if necessary.

The catch with that, though, is if the watch reboots when out of wifi range - we're going to absolutely hammer the battery. Given it should be rare, seems better to accept that it may take up to 30 mins for the data to begin showing.

To support the 30 min update interval (at least without adding an additional ticker), I've had to swap solar back to the even minutes (this is getting ridiculous...) - 30/00 are both even.

verified

mentioned in commit sysconfigs/watchy-code@656bb52e0291641449c7332c7037cf7581b91ce8

Commit: sysconfigs/watchy-code@656bb52e0291641449c7332c7037cf7581b91ce8 
Author: B Tasker                            
                            
Date: 2023-06-17T00:17:29.000+01:00 

Message

Switch solar back to being on even minutes misc/watchy-experimentation#5

This is to ensure it's display aligns with the new 30 minute schedule

+5 -5 (10 lines changed)

9ish hours later, the battery's still on 3 bips. That's a good sign, but we'll have to see how it does throughout the day.

Currently though, the calling function is defined as follows

void Watchy7SEG::drawWatchFace(){

    bool minute_is_even = (currentTime.Minute % 2 == 0);

    display.fillScreen(DARKMODE ? GxEPD_BLACK : GxEPD_WHITE);
    display.setTextColor(DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    drawTime();
    drawDate();
    drawSteps();
    drawWeather(minute_is_even);
    drawBattery();
    display.drawBitmap(120, 77, WIFI_CONFIGURED ? wifi : wifioff, 26, 18, DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    if(BLE_CONFIGURED){
        display.drawBitmap(100, 75, bluetooth, 13, 21, DARKMODE ? GxEPD_WHITE : GxEPD_BLACK);
    }

    if ((currentTime.Minute == 0 || currentTime.Minute == 30) && connectWiFi()) {
        // Trigger solar state update
        if (minute_is_even){
          getSolarState(true);
        }

        // Push stats to InfluxDB
        writeStats();

        // Turn the radios back off
        WiFi.mode(WIFI_OFF); 
        btStop();    
    } else {
        if (minute_is_even){
          getSolarState(false);
        }
    }
}

It's about 33 hours since I last charged the watch, and it's still got 2 bips on the battery.

The latest stats writes into InfluxDB suggest the battery voltage is 3.97v so we're not far off dropping to one bip:

else if(VBAT > 3.80 && VBAT <= 3.95){
        batteryLevel = 1;
    }

I'm going to close this as done.

There are things we could look at in future to improve efficiency further, particularly if we move to using InfluxDB as the source of truth for everything (because then we can send multiple queries in a single request). But, if I decide to play around with that, it's best left to a dedicated ticket.