From 424fce06e8fe50ac11bcfb6b18b282769c2c4a98 Mon Sep 17 00:00:00 2001 From: joseto1298 Date: Fri, 25 Oct 2024 18:03:38 +0200 Subject: [PATCH] Filament backup, new and modified events for printer inventory plugins. --- octoprint_Spoolman/SpoolmanPlugin.py | 10 + octoprint_Spoolman/common/events.py | 3 + octoprint_Spoolman/common/settings.py | 2 + octoprint_Spoolman/modules/PluginAPI.py | 2 + octoprint_Spoolman/modules/PrinterHandler.py | 247 +++++++++++++++++- .../modules/SpoolmanConnector.py | 47 ++++ .../templates/Spoolman_settings.jinja2 | 28 ++ 7 files changed, 336 insertions(+), 3 deletions(-) diff --git a/octoprint_Spoolman/SpoolmanPlugin.py b/octoprint_Spoolman/SpoolmanPlugin.py index 4874085..ae68a9f 100644 --- a/octoprint_Spoolman/SpoolmanPlugin.py +++ b/octoprint_Spoolman/SpoolmanPlugin.py @@ -57,6 +57,7 @@ def triggerPluginEvent(self, eventType, eventPayload = {}): def on_after_startup(self): self._logger.info("[Spoolman][init] Plugin activated") + self.loadSaveSpoolUsage() # Printing events handlers def on_event(self, event, payload): @@ -68,9 +69,13 @@ def on_event(self, event, payload): event == Events.PRINT_CANCELLED ): self.handlePrintingStatusChange(event) + self.resetSaveStatus(event) pass + if event == Events.FILE_SELECTED: + self.handleFileSelected(payload) + def on_sentGCodeHook(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if not self._isInitialized: return @@ -129,6 +134,8 @@ def get_settings_defaults(self): SettingsKeys.SHOW_LOT_NUMBER_COLUMN_IN_SPOOL_SELECT_MODAL: False, SettingsKeys.SHOW_LOT_NUMBER_IN_SIDE_BAR: False, SettingsKeys.SHOW_SPOOL_ID_IN_SIDE_BAR: False, + SettingsKeys.STATUS_BACKUP: False, + SettingsKeys.BACKUP_DATA: None } return settings @@ -144,6 +151,9 @@ def register_custom_events(*args, **kwargs): PluginEvents.SPOOL_SELECTED, PluginEvents.SPOOL_USAGE_COMMITTED, PluginEvents.SPOOL_USAGE_ERROR, + PluginEvents.SPOOL_INFO_ERROR, + PluginEvents.SPOOL_USAGE_COMMITTED_RECOVERY, + PluginEvents.SPOOL_FILE_SELECTED, ] def get_update_information(self): diff --git a/octoprint_Spoolman/common/events.py b/octoprint_Spoolman/common/events.py index 1891511..e351f3e 100644 --- a/octoprint_Spoolman/common/events.py +++ b/octoprint_Spoolman/common/events.py @@ -5,3 +5,6 @@ class PluginEvents(): SPOOL_SELECTED = "spool_selected" SPOOL_USAGE_COMMITTED = "spool_usage_committed" SPOOL_USAGE_ERROR = "spool_usage_error" + SPOOL_INFO_ERROR = "spool_info_error" + SPOOL_USAGE_COMMITTED_RECOVERY = "spool_usage_committed_recovery" + SPOOL_FILE_SELECTED = "spool_file_selected" \ No newline at end of file diff --git a/octoprint_Spoolman/common/settings.py b/octoprint_Spoolman/common/settings.py index f8cf481..3e084a6 100644 --- a/octoprint_Spoolman/common/settings.py +++ b/octoprint_Spoolman/common/settings.py @@ -11,3 +11,5 @@ class SettingsKeys(): SHOW_LOT_NUMBER_COLUMN_IN_SPOOL_SELECT_MODAL = "showLotNumberColumnInSpoolSelectModal" SHOW_LOT_NUMBER_IN_SIDE_BAR = "showLotNumberInSidebar" SHOW_SPOOL_ID_IN_SIDE_BAR = "showSpoolIdInSidebar" + BACKUP_DATA = "backupData" + STATUS_BACKUP = "isstatusBackupEnabled" \ No newline at end of file diff --git a/octoprint_Spoolman/modules/PluginAPI.py b/octoprint_Spoolman/modules/PluginAPI.py index 8dad6af..c401dde 100644 --- a/octoprint_Spoolman/modules/PluginAPI.py +++ b/octoprint_Spoolman/modules/PluginAPI.py @@ -81,6 +81,8 @@ def handleUpdateActiveSpool(self): } ) + self.infoSpool(spoolId,toolId) + return flask.jsonify({ "data": {} }) diff --git a/octoprint_Spoolman/modules/PrinterHandler.py b/octoprint_Spoolman/modules/PrinterHandler.py index 2d46dd0..12df50d 100644 --- a/octoprint_Spoolman/modules/PrinterHandler.py +++ b/octoprint_Spoolman/modules/PrinterHandler.py @@ -12,12 +12,16 @@ def initialize(self): self.lastPrintCancelled = False self.lastPrintOdometer = None self.lastPrintOdometerLoad = None - + self.currentZ = None + self.resetExtruder = False + self.dataSpool = {} + def handlePrintingStatusChange(self, eventType): if eventType == Events.PRINT_STARTED: self.lastPrintCancelled = False self.lastPrintOdometer = gcode() self.lastPrintOdometerLoad = self.lastPrintOdometer._load(None) + self.resetExtruder = False next(self.lastPrintOdometerLoad) @@ -59,6 +63,8 @@ def commitSpoolUsage(self): current_extrusion_stats = copy.deepcopy(peek_stats_helpers['get_current_extrusion_stats']()) peek_stats_helpers['reset_extrusion_stats']() + + self.resetExtruder = True selectedSpoolIds = self._settings.get([SettingsKeys.SELECTED_SPOOL_IDS]) @@ -78,11 +84,25 @@ def commitSpoolUsage(self): selectedSpoolId = selectedSpool['spoolId'] + weight = self.getWeight(str(toolIdx),toolExtrusionLength) + cost = self.getCost(str(toolIdx),weight) + + filament = self.dataSpool.get(str(toolIdx), {}).get('filament', {}) + + name = filament.get('name', None) + material = filament.get('material', None) + colorHex = filament.get('color_hex', None) + self._logger.info( - "Extruder '%s', spool id: %s, usage: %s", + "Extruder '%s', spool id: %s, usage length: %s, weight: %s, cost: %s name: %s material: %s colorHex: %s", toolIdx, selectedSpoolId, - toolExtrusionLength + toolExtrusionLength, + weight, + cost, + name, + material, + colorHex ) result = self.getSpoolmanConnector().handleCommitSpoolUsage(selectedSpoolId, toolExtrusionLength) @@ -101,5 +121,226 @@ def commitSpoolUsage(self): 'toolIdx': toolIdx, 'spoolId': selectedSpoolId, 'extrusionLength': toolExtrusionLength, + 'weight': weight, + 'cost': cost, + 'name': name, + 'material': material, + 'colorHex': colorHex } ) + + def saveFilamentStatusChange(self): + if not self._printer.is_printing() or not self._settings.get_boolean([SettingsKeys.STATUS_BACKUP]): + return + + backup_save = self._settings.get([SettingsKeys.BACKUP_DATA]) + + if backup_save is None: + backup_save = {} + + peek_stats_helpers = self.lastPrintOdometerLoad.send(False) + current_extrusion_stats = copy.deepcopy(peek_stats_helpers['get_current_extrusion_stats']()) + + selectedSpoolIds = self._settings.get([SettingsKeys.SELECTED_SPOOL_IDS]) + + for toolIdx, toolExtrusionLength in enumerate(current_extrusion_stats['extrusionAmount']): + try: + toolIdxStr = str(toolIdx) + selectedSpool = selectedSpoolIds.get(toolIdxStr, None) + + except Exception as e: + self._logger.info("Extruder Saved '%s', spool id: none. Error: %s", toolIdxStr, e) + continue + + if not selectedSpool or selectedSpool.get('spoolId', None) is None: + continue + + selectedSpoolId = selectedSpool['spoolId'] + + weight = self.getWeight(toolIdxStr, toolExtrusionLength) + cost = self.getCost(toolIdxStr, weight) + + filament = self.dataSpool.get(toolIdxStr, {}).get('filament', {}) + + name = filament.get('name', None) + material = filament.get('material', None) + colorHex = filament.get('color_hex', None) + + if toolIdxStr not in backup_save: + backup_save[toolIdxStr] = {} + + if self.resetExtruder == True: + backup_save[toolIdxStr]['spoolId'] = selectedSpoolId + backup_save[toolIdxStr]['name'] = name + backup_save[toolIdxStr]['material'] = material + backup_save[toolIdxStr]['color_hex'] = colorHex + + backup_save[toolIdxStr]['totalExtrusionLength'] = toolExtrusionLength + backup_save[toolIdxStr].get('sessionExtrusionLength', 0) + backup_save[toolIdxStr].get('totalExtrusionLength', 0) + backup_save[toolIdxStr]['sessionExtrusionLength'] = 0 + + backup_save[toolIdxStr]['totalWeight'] = weight + backup_save[toolIdxStr].get('sessionWeight', 0) + backup_save[toolIdxStr].get('totalWeight', 0) + backup_save[toolIdxStr]['sessionWeight'] = 0 + + backup_save[toolIdxStr]['totalCost'] = cost + backup_save[toolIdxStr].get('sessionCost', 0) + backup_save[toolIdxStr].get('totalCost', 0) + backup_save[toolIdxStr]['sessionCost'] = 0 + + if self.resetExtruder == False: + backup_save[toolIdxStr]['spoolId'] = selectedSpoolId + backup_save[toolIdxStr]['name'] = name + backup_save[toolIdxStr]['material'] = material + backup_save[toolIdxStr]['color_hex'] = colorHex + backup_save[toolIdxStr]['sessionExtrusionLength'] = toolExtrusionLength + backup_save[toolIdxStr]['sessionWeight'] = weight + backup_save[toolIdxStr]['sessionCost'] = cost + + self.resetExtruder = False + + self._settings.set([SettingsKeys.BACKUP_DATA], backup_save) + self._settings.save() + + def loadSaveSpoolUsage(self): + backup_data = self._settings.get([SettingsKeys.BACKUP_DATA]) + + if not isinstance(backup_data, dict): + return + + for toolIdx, spool_data in backup_data.items(): + toolIdxStr = str(toolIdx) + selectedSpoolId = spool_data.get('spoolId') + name = spool_data.get('name') + material = spool_data.get('material') + colorHex = spool_data.get('color_hex') + + toolExtrusionLength = spool_data.get('totalExtrusionLength', 0) + spool_data.get('sessionExtrusionLength', 0) + weight = spool_data.get('totalWeight', 0) + spool_data.get('sessionWeight', 0) + cost = spool_data.get('totalCost', 0) + spool_data.get('sessionCost', 0) + + if not selectedSpoolId or toolExtrusionLength is None: + self._logger.info("Loading saved spool usage: Extruder '%s', spool id: none or invalid data", toolIdxStr) + continue + + result = self.getSpoolmanConnector().handleCommitSpoolUsage(selectedSpoolId, toolExtrusionLength) + + if result.get('error', None): + self.triggerPluginEvent( + Events.PLUGIN_SPOOLMAN_SPOOL_USAGE_ERROR, + result['error'] + ) + return + + self.triggerPluginEvent( + Events.PLUGIN_SPOOLMAN_SPOOL_USAGE_COMMITTED_RECOVERY, + { + 'toolIdx': toolIdxStr, + 'spoolId': selectedSpoolId, + 'extrusionLength': toolExtrusionLength, + 'weight': weight, + 'cost': cost, + 'name': name, + 'material': material, + 'colorHex': colorHex + } + ) + + + self._settings.set([SettingsKeys.BACKUP_DATA], None) + self._settings.save() + + def resetSaveStatus(self,eventType): + if ( + eventType == Events.PRINT_DONE or + eventType == Events.PRINT_CANCELLED and + self._settings.get_boolean([SettingsKeys.STATUS_BACKUP]) + ): + self._settings.set([SettingsKeys.BACKUP_DATA], None) + self._settings.save() + + def infoSpool(self,SpoolId,toolId): + if not hasattr(self, 'dataSpool') or self.dataSpool is None: + self.dataSpool = {} + + if SpoolId is None: + return + + result = self.getSpoolmanConnector().handleCommitSpoolInfo(SpoolId) + + if result.get('error', None): + self.triggerPluginEvent( + Events.PLUGIN_SPOOLMAN_SPOOL_INFO_ERROR, + result['error'] + ) + return + + self.dataSpool[toolId] = result + + def handleFileSelected(self, payload): + self._logger.info("File selected" + str(payload)) + selectedSpoolIds = self._settings.get([SettingsKeys.SELECTED_SPOOL_IDS]) + jobFilamentUsage = self.getCurrentJobFilamentUsage() + + for toolIdx, spoolData in selectedSpoolIds.items(): + selectedSpoolId = spoolData.get('spoolId', None) + + if selectedSpoolId: + self.infoSpool(selectedSpoolId, toolIdx) + + else: + self._logger.warning("No spoolId found for extruder %s", toolIdx) + + if jobFilamentUsage['jobHasFilamentLengthData']: + for toolIndex, filamentLength in enumerate(jobFilamentUsage["jobFilamentLengthsPerTool"]): + + weight = self.getWeight(str(toolIndex), filamentLength) + cost = self.getCost(str(toolIndex), weight) + + filament = self.dataSpool.get(str(toolIndex), {}).get('filament', {}) + + name = filament.get('name', None) + material = filament.get('material', None) + colorHex = filament.get('color_hex', None) + + self.triggerPluginEvent( + Events.PLUGIN_SPOOLMAN_SPOOL_FILE_SELECTED, + { + 'toolIdx': str(toolIndex), + 'spoolId': self.dataSpool.get(str(toolIndex), {}).get('id', None), + 'estimatedExtrusionLength': filamentLength, + 'estimatedWeight': weight, + 'estimatedCost': cost, + 'name': name, + 'material': material, + 'colorHex': colorHex + } + ) + + def getWeight(self,toolIdx,toolExtrusionLength): + if toolIdx not in self.dataSpool: + return 0 + + spool_data = self.dataSpool[toolIdx]['filament'] + + density = spool_data.get('density', None) + diameter = spool_data.get('diameter', None) + + if density is None or diameter is None: + return 0 + + weight = self.getFilamentWeight(toolExtrusionLength, density, diameter) + + return weight + + def getCost(self,toolIdx,weight): + if toolIdx not in self.dataSpool: + return 0 + + spool_data = self.dataSpool[toolIdx] + + initial_weight = spool_data.get('initial_weight', 0) + price = spool_data.get('price', 0) + + if initial_weight <= 0 or price <= 0: + return 0 + + cost_use = (weight / initial_weight) * price + + return cost_use \ No newline at end of file diff --git a/octoprint_Spoolman/modules/SpoolmanConnector.py b/octoprint_Spoolman/modules/SpoolmanConnector.py index 806b749..27dd5a4 100644 --- a/octoprint_Spoolman/modules/SpoolmanConnector.py +++ b/octoprint_Spoolman/modules/SpoolmanConnector.py @@ -163,3 +163,50 @@ def handleCommitSpoolUsage(self, spoolId, spoolUsedLength): return { "data": {} } + + def handleCommitSpoolInfo(self, spoolId): + precheckResult = self._precheckSpoolman() + + if precheckResult and precheckResult.get('error', False): + return precheckResult + + spoolIdStr = str(spoolId) + endpointUrl = self._createSpoolmanEndpointUrl("/spool/" + spoolIdStr) + + self._logSpoolmanCall(endpointUrl) + + try: + session = requests.Session() + session.verify = self.verifyConfig + retries = Retry(total = 3, backoff_factor = 1, status_forcelist = [ 500, 502, 503, 504 ]) + + session.mount(self.instanceUrl, HTTPAdapter(max_retries=retries)) + + response = session.get( + url = endpointUrl, + json = {}, + timeout = 1 + ) + except Exception as caughtException: + return self._handleSpoolmanConnectionError(caughtException) + + if response.status_code == 404: + return self._handleSpoolmanError( + response, + { + "code": "spoolman_api__spool_not_found", + "spoolman_api": { + "status_code": response.status_code, + }, + "data": { + "spoolId": spoolIdStr, + }, + } + ) + + if response.status_code != 200: + return self._handleSpoolmanError(response) + + self._logSpoolmanSuccess(response) + + return response.json() \ No newline at end of file diff --git a/octoprint_Spoolman/templates/Spoolman_settings.jinja2 b/octoprint_Spoolman/templates/Spoolman_settings.jinja2 index 04a79b5..99877c9 100644 --- a/octoprint_Spoolman/templates/Spoolman_settings.jinja2 +++ b/octoprint_Spoolman/templates/Spoolman_settings.jinja2 @@ -123,6 +123,34 @@ +
+
+
+
+ +
+
+ +
+
+ + Enabling the backup function allows you to store relevant information during the current + printing + process and generate an event with the print data. In particular, it records the grams + of material used + by the tools up to the moment a failure occurs. If this option is enabled and a failure + occurs, the + system will automatically update the data of the failed print when starting again, + avoiding loss of + information. For low-performance equipment, it is recommended to keep this option + disabled. + +
+
+