updated: only fetch on metered connection when necessary
diff --git a/common/params.cc b/common/params.cc
index b1fc15e4c5dfbc..b416b801a8db95 100644
--- a/common/params.cc
+++ b/common/params.cc
@@ -161,6 +161,7 @@ std::unordered_map<std::string, uint32_t> keys = {
     {"NavPastDestinations", PERSISTENT},
     {"NavSettingLeftSide", PERSISTENT},
     {"NavSettingTime24h", PERSISTENT},
+    {"NetworkMetered", PERSISTENT},
     {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
     {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
     {"Offroad_BadNvme", CLEAR_ON_MANAGER_START},
@@ -201,6 +202,7 @@ std::unordered_map<std::string, uint32_t> keys = {
     {"UpdaterNewReleaseNotes", CLEAR_ON_MANAGER_START},
     {"UpdaterState", CLEAR_ON_MANAGER_START},
     {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START},
+    {"UpdaterLastFetchTime", PERSISTENT},
     {"Version", PERSISTENT},
     {"VisionRadarToggle", PERSISTENT},
     {"WheeledBody", PERSISTENT},
diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py
index 7011ff0a99d7a6..75e091febbbd36 100755
--- a/selfdrive/thermald/thermald.py
+++ b/selfdrive/thermald/thermald.py
@@ -447,6 +447,8 @@ def thermald_thread(end_event, hw_queue) -> None:
         except Exception:
           cloudlog.exception("failed to save offroad status")
 
+    params.put_bool_nonblocking("NetworkMetered", (msg.deviceState.networkType != NetworkType.wifi))
+
     count += 1
     should_start_prev = should_start
 
diff --git a/selfdrive/updated.py b/selfdrive/updated.py
index a623aaefc83612..8a46a11a78612d 100755
--- a/selfdrive/updated.py
+++ b/selfdrive/updated.py
@@ -35,26 +35,42 @@
 DAYS_NO_CONNECTIVITY_MAX = 14     # do not allow to engage after this many days
 DAYS_NO_CONNECTIVITY_PROMPT = 10  # send an offroad prompt after this many days
 
+class UserRequest:
+  NONE = 0
+  CHECK = 1
+  FETCH = 2
+
 class WaitTimeHelper:
   def __init__(self):
     self.ready_event = threading.Event()
-    self.only_check_for_update = False
+    self.user_request = UserRequest.NONE
     signal.signal(signal.SIGHUP, self.update_now)
     signal.signal(signal.SIGUSR1, self.check_now)
 
   def update_now(self, signum: int, frame) -> None:
     cloudlog.info("caught SIGHUP, attempting to downloading update")
-    self.only_check_for_update = False
+    self.user_request = UserRequest.FETCH
     self.ready_event.set()
 
   def check_now(self, signum: int, frame) -> None:
     cloudlog.info("caught SIGUSR1, checking for updates")
-    self.only_check_for_update = True
+    self.user_request = UserRequest.CHECK
     self.ready_event.set()
 
   def sleep(self, t: float) -> None:
     self.ready_event.wait(timeout=t)
 
+def write_time_to_param(params, param) -> None:
+  t = datetime.datetime.utcnow()
+  params.put(param, t.isoformat().encode('utf8'))
+
+def read_time_from_param(params, param) -> Optional[datetime.datetime]:
+  t = params.get(param, encoding='utf8')
+  try:
+    return datetime.datetime.fromisoformat(t)
+  except (TypeError, ValueError):
+    pass
+  return None
 
 def run(cmd: List[str], cwd: Optional[str] = None) -> str:
   return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8')
@@ -266,14 +282,11 @@ def set_params(self, update_success: bool, failed_count: int, exception: Optiona
 
     last_update = datetime.datetime.utcnow()
     if update_success:
-      t = last_update.isoformat()
-      self.params.put("LastUpdateTime", t.encode('utf8'))
+      write_time_to_param(self.params, "LastUpdateTime")
     else:
-      try:
-        t = self.params.get("LastUpdateTime", encoding='utf8')
-        last_update = datetime.datetime.fromisoformat(t)
-      except (TypeError, ValueError):
-        pass
+      t = read_time_from_param(self.params, "LastUpdateTime")
+      if t is not None:
+        last_update = t
 
     if exception is None:
       self.params.remove("LastUpdateException")
@@ -421,10 +434,7 @@ def main() -> None:
 
   updater = Updater()
   update_failed_count = 0 # TODO: Load from param?
-
-  # no fetch on the first time
   wait_helper = WaitTimeHelper()
-  wait_helper.only_check_for_update = True
 
   # invalidate old finalized update
   set_consistent_flag(False)
@@ -458,10 +468,16 @@ def main() -> None:
       updater.check_for_update()
 
       # download update
-      if wait_helper.only_check_for_update:
-        cloudlog.info("skipping fetch this cycle")
+      last_fetch = read_time_from_param(params, "UpdaterLastFetchTime")
+      timed_out = last_fetch is None or (datetime.datetime.utcnow() - last_fetch > datetime.timedelta(days=3))
+      user_requested_fetch = wait_helper.user_request == UserRequest.FETCH
+      if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch:
+        cloudlog.info("skipping fetch, connection metered")
+      elif wait_helper.user_request == UserRequest.CHECK:
+        cloudlog.info("skipping fetch, only checking")
       else:
         updater.fetch_update()
+        write_time_to_param(params, "UpdaterLastFetchTime")
       update_failed_count = 0
     except subprocess.CalledProcessError as e:
       cloudlog.event(
@@ -485,7 +501,7 @@ def main() -> None:
       cloudlog.exception("uncaught updated exception while setting params, shouldn't happen")
 
     # infrequent attempts if we successfully updated recently
-    wait_helper.only_check_for_update = False
+    wait_helper.user_request = UserRequest.NONE
     wait_helper.sleep(5*60 if update_failed_count > 0 else 1.5*60*60)
 
 

- [x] metered can wait a few days
- [x] same policy for non-metered connections
- [x] download button in UI always fetches