Skip to content

Commit

Permalink
#96 On DPMS wake, check schedule is setup. Apply locking to schedulin…
Browse files Browse the repository at this point in the history
…g to ensure only one scheduling update can run at a time.
  • Loading branch information
digitaltrails committed Oct 25, 2024
1 parent 60f3520 commit 8f7d7be
Showing 1 changed file with 53 additions and 40 deletions.
93 changes: 53 additions & 40 deletions vdu_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7509,9 +7509,9 @@ def configure_application(self, main_window: VduAppWindow | None = None):
if self.main_window.main_panel is not None:
self.main_window.indicate_busy(True)
QApplication.processEvents()
log_debug("configure: try to obtain application_configuration_lock", trace=False) if log_debug_enabled else None
log_debug("configure: try to obtain application_lock", trace=False) if log_debug_enabled else None
with self.application_lock:
log_debug("Holding application_configuration_lock") if log_debug_enabled else None
log_debug("Holding application_lock") if log_debug_enabled else None
self.unschedule_presets() # Hopefully stops any timers from firing.
if self.lux_auto_controller is not None:
self.lux_auto_controller.stop_worker()
Expand All @@ -7526,15 +7526,15 @@ def configure_application(self, main_window: VduAppWindow | None = None):
self.main_window.create_main_control_panel()
SettingsEditor.reconfigure_instance(self.get_vdu_configs())
self.restore_vdu_initialization_presets()
self.schedule_presets(True)

log_debug("configure: released application_configuration_lock") if log_debug_enabled else None
log_debug("configure: released application_lock") if log_debug_enabled else None
if self.main_config.is_set(ConfOption.LUX_OPTIONS_ENABLED):
if self.lux_auto_controller is not None:
self.lux_auto_controller.initialize_from_config()
LuxDialog.reconfigure_instance()
self.main_window.update_status_indicators()
# restore_preset tries to acquire the same lock, safe to unlock and let it relock...
self.schedule_presets(True)
self.activate_overdue_preset()
finally:
if self.main_window is not None:
Expand Down Expand Up @@ -7705,6 +7705,8 @@ def update_ui_view(worker: WorkerThread) -> None:
log_info(f"Reconfiguring: detected={self.detected_vdu_list} previously={self.previously_detected_vdu_list}")
self.configure_application() # May cause a further refresh?
self.previously_detected_vdu_list = self.detected_vdu_list
else:
self.check_preset_schedule()
self.activate_overdue_preset()
if self.lux_auto_controller:
if LuxDialog.exists():
Expand Down Expand Up @@ -7737,9 +7739,9 @@ def restore_preset(self, preset: Preset, finished_func: Callable[[PresetTransiti
immediately, background_activity, initialization_preset))
return

log_debug("restore_preset: try to obtain application_configuration_lock", trace=False) if log_debug_enabled else None
log_debug("restore_preset: try to obtain application_lock", trace=False) if log_debug_enabled else None
with self.application_lock: # The lock prevents a transition firing when the GUI/app is reconfiguring
log_debug("restore_preset: holding application_configuration_lock", trace=False) if log_debug_enabled else None
log_debug("restore_preset: holding application_lock", trace=False) if log_debug_enabled else None
self.transitioning_dummy_preset = None
if not immediately:
self.transitioning_dummy_preset = PresetTransitionDummy(preset)
Expand Down Expand Up @@ -7782,7 +7784,7 @@ def restore_finished_callback(worker_thread: PresetTransitionWorker) -> None:
worker.start()
if initialization_preset:
worker.wait()
log_debug("restore_preset: released application_configuration_lock") if log_debug_enabled else None
log_debug("restore_preset: released application_lock") if log_debug_enabled else None


def restore_vdu_initialization_presets(self):
Expand Down Expand Up @@ -7812,40 +7814,43 @@ def schedule_presets(self, reconfiguring: bool = False) -> None:
assert is_running_in_gui_thread() # Needs to be run in the GUI thread so the timers also run in the GUI thread
location = self.main_config.get_location()
if location and self.main_config.is_set(ConfOption.SCHEDULE_ENABLED):
local_now = zoned_now()
## local_now = local_now.replace(hour=23, minute=59, second=59) # For testing
start_of_next_day = (local_now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
if abs(start_of_next_day - local_now).seconds < 10: # event fired early? Need to round-up to start of day.
log_info(f"Scheduling presets, altering {local_now} to correct day {start_of_next_day}")
local_now = start_of_next_day
log_info(f"Scheduling presets for {local_now} {reconfiguring=}")
time_map = create_elevation_map(local_now, latitude=location.latitude, longitude=location.longitude)
for preset in self.preset_controller.find_presets_map().values():
if reconfiguring:
preset.remove_elevation_trigger(quietly=True) # Also resets schedule_status
elevation_key = preset.get_solar_elevation()
if elevation_key is not None and preset.schedule_status == PresetScheduleStatus.UNSCHEDULED:
if elevation_data := time_map.get(elevation_key):
when_today = elevation_data.when
preset.elevation_time_today = when_today
if when_today > local_now:
preset.start_timer(when_today, self.activate_scheduled_preset)
log_debug("schedule_presets: try to obtain application_lock") if log_debug_enabled else None
with self.application_lock:
log_debug("schedule_presets: holding application_lock") if log_debug_enabled else None
local_now = zoned_now()
## local_now = local_now.replace(hour=23, minute=59, second=59) # For testing
start_of_next_day = (local_now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
if abs(start_of_next_day - local_now).seconds < 10: # event fired early? Need to round-up to start of day.
log_info(f"Scheduling presets, altering {local_now} to correct day {start_of_next_day}")
local_now = start_of_next_day
log_info(f"Scheduling presets for {local_now} {reconfiguring=}")
time_map = create_elevation_map(local_now, latitude=location.latitude, longitude=location.longitude)
for preset in self.preset_controller.find_presets_map().values():
if reconfiguring:
preset.remove_elevation_trigger(quietly=True) # Also resets schedule_status
elevation_key = preset.get_solar_elevation()
if elevation_key is not None and preset.schedule_status == PresetScheduleStatus.UNSCHEDULED:
if elevation_data := time_map.get(elevation_key):
when_today = elevation_data.when
preset.elevation_time_today = when_today
if when_today > local_now:
preset.start_timer(when_today, self.activate_scheduled_preset)
else:
log_info(f"Skipped preset {preset.name}, passed assigned-time (status={preset.schedule_status})")
if reconfiguring and preset.schedule_status != PresetScheduleStatus.SUCCEEDED: # Not already succeeded
preset.schedule_status = PresetScheduleStatus.SKIPPED_SUPERSEDED
else:
log_info(f"Skipped preset {preset.name}, passed assigned-time (status={preset.schedule_status})")
if reconfiguring and preset.schedule_status != PresetScheduleStatus.SUCCEEDED: # Not already succeeded
preset.schedule_status = PresetScheduleStatus.SKIPPED_SUPERSEDED
else:
log_info(f"Skipped preset {preset.name} {elevation_key} degrees,"
" the sun does not reach that elevation today.")
# set a timer to rerun this at the start of the next day. Add extra 30s to avoid timer-accuracy and truncation-errors.
tomorrow = zoned_now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
if self.daily_schedule_next_update != tomorrow:
millis = (tomorrow - zoned_now()) / timedelta(milliseconds=1) # Possible truncation error in the math?
## millis = 1000 * 60 * 10 # For testing
log_info(f"Will update schedule for solar-activation at {tomorrow} (in {round(millis / 1000 / 60)} minutes)")
QTimer.singleShot(int(millis), partial(self.schedule_presets, True)) # type: ignore
# Testing: QTimer.singleShot(int(1000*30), partial(self.schedule_presets, True))
self.daily_schedule_next_update = tomorrow
log_info(f"Skipped preset {preset.name} {elevation_key} degrees,"
" the sun does not reach that elevation today.")
# set a timer to rerun this at the start of the next day. Add extra 30s to avoid timer-accuracy and truncation-errors.
tomorrow = zoned_now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
if self.daily_schedule_next_update != tomorrow:
millis = (tomorrow - zoned_now()) / timedelta(milliseconds=1) # Possible truncation error in the math?
## millis = 1000 * 60 * 1 # For testing
log_info(f"Will update schedule for solar-activation at {tomorrow} (in {round(millis / 1000 / 60)} minutes)")
QTimer.singleShot(int(millis), partial(self.schedule_presets, True)) # type: ignore
self.daily_schedule_next_update = tomorrow
log_debug("schedule_presets: released application_lock") if log_debug_enabled else None
else:
log_info(f"Scheduling is disabled or no location ({location=})")
if reconfiguring:
Expand All @@ -7856,6 +7861,14 @@ def unschedule_presets(self):
if preset.timer is not None and preset.timer.remainingTime() > 0:
preset.timer.stop()

def check_preset_schedule(self):
if self.main_config.is_set(ConfOption.SCHEDULE_ENABLED):
now = zoned_now()
with self.application_lock:
if self.daily_schedule_next_update is None or self.daily_schedule_next_update < now:
log_info("check_preset_schedule: schedule appears to be out of date - refresh it...")
self.schedule_presets(True)

def activate_overdue_preset(self):
if not self.main_config.is_set(ConfOption.SCHEDULE_ENABLED):
log_info(f"activate_overdue_preset: skipped - scheduling is disabled")
Expand Down

0 comments on commit 8f7d7be

Please sign in to comment.