diff --git a/octoapp/notificationsender.py b/octoapp/notificationsender.py index 718fff4..150d959 100644 --- a/octoapp/notificationsender.py +++ b/octoapp/notificationsender.py @@ -52,6 +52,7 @@ def SendNotification(self, event, state=None): if event == self.EVENT_DONE: state["ProgressPercentage"] = 100 + self.LastPrintState = state helper = AppStorageHelper.Get() Sentry.Info("SENDER", "Preparing notification for %s" % event) @@ -68,13 +69,20 @@ def SendNotification(self, event, state=None): if not targets: Sentry.Debug("SENDER", "No targets, skipping notification") return - + + target_count_before_filter = len(targets) targets = self._processFilters(targets=targets, event=event) ios_targets = helper.GetIosApps(targets) activity_targets = helper.GetActivities(targets) android_targets = helper.GetAndroidApps(targets) apnsData = self._createApnsPushData(event, state) if len(ios_targets) or len(activity_targets) else None + # Some clients might have user interaction disbaled. First send pause so all live activities etc + if event == self.EVENT_USER_INTERACTION_NEEDED and target_count_before_filter != len(targets): + Sentry.Info("SENDER", "User interaction needed, first sending pause") + self.SendNotification(self.EVENT_PAUSED) + time.sleep(2) + if not len(android_targets) and apnsData is None: Sentry.Info("SENDER", "Skipping push, no Android targets and no APNS data, skipping notification") return @@ -138,6 +146,14 @@ def _processFilters(self, targets, event): filterName = "layer_1" elif event == self.EVENT_THIRD_LAYER_DONE: filterName = "layer_3" + elif event == self.EVENT_FILAMENT_REQUIRED: + filterName = "filament_required" + elif event == self.EVENT_ERROR: + filterName = "error" + elif event == self.EVENT_USER_INTERACTION_NEEDED: + filterName = "interaction" + elif event == self.EVENT_BEEP: + filterName = "beep" else: return targets @@ -312,7 +328,7 @@ def _createApnsPushData(self, event, state): notificationTitle = "Filament required" notificationTitleKey = "print_notification___filament_change_required_title" notificationTitleArgs = [self.PrinterName] - notificationBody = tate.get("FileName", None) + notificationBody = state.get("FileName", None) notificationSound = "notification_filament_change.wav" liveActivityState = "filamentRequired" diff --git a/octoapp/notificationshandler.py b/octoapp/notificationshandler.py index 6071429..be748ac 100644 --- a/octoapp/notificationshandler.py +++ b/octoapp/notificationshandler.py @@ -58,6 +58,7 @@ def __init__(self, printerStateInterface): self.ProgressTimer = None self.FirstLayerTimer = None self.FinalSnapObj:FinalSnap = None + self.PauseThread = None # self.Gadget = Gadget(logger, self, self.PrinterStateInterface) # Define all the vars @@ -219,6 +220,11 @@ def OnRestorePrintIfNeeded(self, moonrakerPrintStatsState, fileName_CanBeNone, t # On paused, make sure they are stopped. self.StopTimers() + def _cancelDelayedPause(self): + if self.PauseThread is not None and self.PauseThread.is_alive(): + Sentry.Info("NOTIFICATION", "Cancelling delayed pause") + self.PauseThread.stop() + self.PauseThread = None # Only used for testing. def OnTest(self): @@ -261,6 +267,7 @@ def OnFailed(self, fileName, durationSecStr, reason): self._updateCurrentFileName(fileName) self._updateToKnownDuration(durationSecStr) self.StopTimers() + self._cancelDelayedPause() self._sendEvent(NotificationSender.EVENT_CANCELLED, { "Reason": reason}) @@ -272,6 +279,7 @@ def OnDone(self, fileName_CanBeNone, durationSecStr_CanBeNone): self._updateCurrentFileName(fileName_CanBeNone) self._updateToKnownDuration(durationSecStr_CanBeNone) self.StopTimers() + self.__cancelDelayedPause() self._sendEvent(NotificationSender.EVENT_DONE, useFinalSnapSnapshot=True) @@ -279,13 +287,37 @@ def OnDone(self, fileName_CanBeNone, durationSecStr_CanBeNone): def OnPaused(self, fileName): if self._shouldIgnoreEvent(fileName): return + + def firePause(delay, event): + _self = self.PauseThread + Sentry.Info("NOTIFICATION", "Delaying pause for %d seconds" % delay) + time.sleep(delay) + if _self.stopped() is False: + Sentry.Info("NOTIFICATION", "Delayed pause not stopped, executing") + self._sendEvent(event) + self.PauseThread = None + else: + Sentry.Info("NOTIFICATION", "Delayed pause was stopped, dropping") + + def scheduleSent(delay, event): + if self.PauseThread is None or self.PauseThread.is_alive() is False: + if delay == 0: + self._sendEvent(event) + else: + self.PauseThread = StoppableThread(target=firePause, args=(delay, event)) + self.PauseThread.start() + else: + Sentry.Error("NOTIFICATION", "Skipping pause, already scheduled") # Always update the file name. self._updateCurrentFileName(fileName) + delay = 0 event = NotificationSender.EVENT_PAUSED if Compat.IsMoonraker(): # Because filament runout doesn't work, let's treat every pause as "interaction needed" + # Also delay in case of timlapse photo the pause will be super short. If the print is resumed within the delay, drop the pause + delay = 3 event = NotificationSender.EVENT_USER_INTERACTION_NEEDED # See if there is a pause notification suppression set. If this is not null and it was recent enough @@ -294,20 +326,21 @@ def OnPaused(self, fileName): if Compat.HasSmartPauseInterface(): lastSuppressTimeSec = Compat.GetSmartPauseInterface().GetAndResetLastPauseNotificationSuppressionTimeSec() if lastSuppressTimeSec is None or time.time() - lastSuppressTimeSec > 20.0: - self._sendEvent(event) + scheduleSent(delay, event) else: Sentry.Info("NOTIFICATION", "Not firing the pause notification due to a Smart Pause suppression.") else: - self._sendEvent(event) + scheduleSent(delay, event) # Stop the ping timer, so we don't report progress while we are paused. self.StopTimers() - # Fired when a print is resumed def OnResume(self, fileName): if self._shouldIgnoreEvent(fileName): return + + self._cancelDelayedPause() self._updateCurrentFileName(fileName) self._sendEvent(NotificationSender.EVENT_RESUME) @@ -324,6 +357,7 @@ def OnError(self, error): return self.StopTimers() + self._cancelDelayedPause() # This might be spammy from OctoPrint, so limit how often we bug the user with them. if self._shouldSendSpammyEvent("on-error"+str(error), 30.0) is False: @@ -1208,6 +1242,19 @@ def _shouldIgnoreEvent(self, fileName:str = None) -> bool: return True return False +class StoppableThread(threading.Thread): + """Thread class with a stop() method. The thread itself has to check + regularly for the stopped() condition.""" + + def __init__(self, *args, **kwargs): + super(StoppableThread, self).__init__(*args, **kwargs) + self._stop_event = threading.Event() + + def stop(self): + self._stop_event.set() + + def stopped(self): + return self._stop_event.is_set() class SpammyEventContext: diff --git a/setup.py b/setup.py index 3cbd1ed..3013b7f 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module # Note that this is also parsed by the moonraker module to pull the version, so the string and format must remain the same! -plugin_version = "2.0.8" +plugin_version = "2.0.9" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module