From ba43df279d4e4d6529fed83bb42ee5d57985efb4 Mon Sep 17 00:00:00 2001 From: Manuel Weiser Date: Sun, 2 Mar 2025 11:58:52 +0100 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20Mock-FTPS-Client-Implementierung=20?= =?UTF-8?q?hinzu,=20um=20FTP-Zugriffe=20zu=20simulieren;=20erweitere=20Feh?= =?UTF-8?q?lerbehandlung=20und=20aktualisiere=20die=20Dateiliste=20mit=20M?= =?UTF-8?q?ock-Dateien.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../printer/bambu_virtual_printer.py | 122 +++++++++--------- .../printer/file_system/cached_file_view.py | 120 +++++++---------- .../file_system/remote_sd_card_file_list.py | 61 ++++++++- 3 files changed, 166 insertions(+), 137 deletions(-) diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index 71eaf3e..3319e7c 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -304,14 +304,18 @@ class BambuVirtualPrinter: print_data = payload['print'] self._log.info(f"Processing print data with keys: {list(print_data.keys())}") - # Temperaturdaten verarbeiten - if 'temperature' in print_data: - self._process_temperature_data(print_data['temperature']) + # Temperaturdaten direkt verarbeiten, ohne auf "temperature" als Schlüssel zu warten + # Bambu-Drucker verwenden Schlüssel wie "nozzle_temper" direkt im print-Objekt + self._process_direct_temperature_data(print_data) # Status verarbeiten if 'gcode_state' in print_data: self._process_print_state(print_data['gcode_state']) + # Fortschritt verarbeiten + if 'mc_percent' in print_data: + self._process_progress_data(print_data) + # Trigger update self.new_update("event_printer_data_update") @@ -327,79 +331,79 @@ class BambuVirtualPrinter: except Exception as e: self._log.error(f"Error processing MQTT payload: {e}", exc_info=True) - def _process_temperature_data(self, temp_data): - """Verarbeitet Temperaturdaten aus MQTT-Nachrichten""" + def _process_direct_temperature_data(self, print_data): + """Verarbeitet Temperaturdaten direkt aus dem print-Objekt""" try: - # Extruder Temperatur - if 'nozzle_temp' in temp_data: - self._telemetry.temp[0] = float(temp_data['nozzle_temp']) - if 'target_nozzle_temp' in temp_data: - self._telemetry.targetTemp[0] = float(temp_data['target_nozzle_temp']) + # Extruder Temperatur - direkt aus den Feldern + if 'nozzle_temper' in print_data: + self._telemetry.temp[0] = float(print_data['nozzle_temper']) + self._log.debug(f"Updated nozzle temperature: {self._telemetry.temp[0]}") + if 'nozzle_target_temper' in print_data: + self._telemetry.targetTemp[0] = float(print_data['nozzle_target_temper']) + self._log.debug(f"Updated nozzle target: {self._telemetry.targetTemp[0]}") # Bett Temperatur - if 'bed_temp' in temp_data: - self._telemetry.bedTemp = float(temp_data['bed_temp']) - if 'target_bed_temp' in temp_data: - self._telemetry.bedTargetTemp = float(temp_data['target_bed_temp']) + if 'bed_temper' in print_data: + self._telemetry.bedTemp = float(print_data['bed_temper']) + self._log.debug(f"Updated bed temperature: {self._telemetry.bedTemp}") + if 'bed_target_temper' in print_data: + self._telemetry.bedTargetTemp = float(print_data['bed_target_temper']) + self._log.debug(f"Updated bed target: {self._telemetry.bedTargetTemp}") # Kammer Temperatur - if 'chamber_temp' in temp_data: - self._telemetry.chamberTemp = float(temp_data['chamber_temp']) + if 'chamber_temper' in print_data: + self._telemetry.chamberTemp = float(print_data['chamber_temper']) + self._log.debug(f"Updated chamber temperature: {self._telemetry.chamberTemp}") - self._log.debug(f"Updated temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " + + # Log der aktualisierten Temperaturen + self._log.debug(f"Current temperatures - Nozzle: {self._telemetry.temp[0]}/{self._telemetry.targetTemp[0]}, " + f"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " + f"Chamber: {self._telemetry.chamberTemp}") + + # Auch im BambuClient aktualisieren + if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'temperature'): + try: + temp_obj = self._bambu_client.device.temperature + if 'nozzle_temper' in print_data: + temp_obj.nozzle_temp = float(print_data['nozzle_temper']) + if 'nozzle_target_temper' in print_data: + temp_obj.target_nozzle_temp = float(print_data['nozzle_target_temper']) + if 'bed_temper' in print_data: + temp_obj.bed_temp = float(print_data['bed_temper']) + if 'bed_target_temper' in print_data: + temp_obj.target_bed_temp = float(print_data['bed_target_temper']) + if 'chamber_temper' in print_data: + temp_obj.chamber_temp = float(print_data['chamber_temper']) + except Exception as e: + self._log.error(f"Error updating BambuClient temperature: {e}") + except Exception as e: self._log.error(f"Error processing temperature data: {e}", exc_info=True) - def _process_print_state(self, print_job_state): - """Verarbeitet den Druckerstatus aus MQTT-Nachrichten""" + def _process_progress_data(self, print_data): + """Verarbeitet Fortschrittsdaten aus MQTT-Nachrichten""" try: - self._log.debug(f"Received printer state update: {print_job_state}") - - # Normalisieren des Status, falls er 'unknown' ist - if print_job_state == "unknown": - # Wenn der Status unbekannt ist, versuchen wir, ihn aus anderen Informationen abzuleiten - if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): - # Prüfe ob Druckfortschritt vorhanden ist - if hasattr(self._bambu_client.device.print_job, 'mc_percent') and self._bambu_client.device.print_job.mc_percent > 0: - print_job_state = "RUNNING" - self._log.debug(f"Changed unknown state to RUNNING based on print progress") - - if print_job_state in ["IDLE", "FINISH", "FAILED"]: - self.change_state(self._state_idle) - elif print_job_state in ["RUNNING", "PREPARE"]: - self.change_state(self._state_printing) - elif print_job_state == "PAUSE": - self.change_state(self._state_paused) - elif print_job_state == "unknown": - # Wenn wir keine bessere Information haben, betrachten wir es als IDLE - self._log.debug("Keeping current state due to unknown printer state") - else: - self._log.warn(f"Unknown print job state: {print_job_state}") + progress = -1 + if 'mc_percent' in print_data: + progress = int(print_data['mc_percent']) + + remaining_time = 0 + if 'mc_remaining_time' in print_data: + remaining_time = int(print_data['mc_remaining_time']) + + # Aktualisiere den PrintJob, wenn einer existiert + if self.current_print_job is not None: + self.current_print_job.print_percentage = progress + self.current_print_job.remaining_time = remaining_time + self._log.debug(f"Updated print progress: {progress}%, remaining: {remaining_time}s") # Aktualisiere auch die pybambu-Datenstruktur if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): - self._bambu_client.device.print_job.gcode_state = print_job_state - except Exception as e: - self._log.error(f"Error processing print state: {e}", exc_info=True) - - def _process_hms_errors(self, hms_data): - """Verarbeitet HMS-Fehlerdaten aus MQTT-Nachrichten""" - try: - if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'hms'): - self._bambu_client.device.hms.update_from_payload(hms_data) - self._log.debug("HMS error data updated") + self._bambu_client.device.print_job.mc_percent = progress + self._bambu_client.device.print_job.mc_remaining_time = remaining_time - # Überprüfe auf Fehler und zeige sie an - if self._bambu_client.device.hms.errors != self._last_hms_errors and self._bambu_client.device.hms.errors["Count"] > 0: - self._log.debug(f"HMS Error: {self._bambu_client.device.hms.errors}") - for n in range(1, self._bambu_client.device.hms.errors["Count"] + 1): - error = self._bambu_client.device.hms.errors[f"{n}-Error"].strip() - self.sendIO(f"// action:notification {error}") - self._last_hms_errors = self._bambu_client.device.hms.errors except Exception as e: - self._log.error(f"Error processing HMS errors: {e}", exc_info=True) + self._log.error(f"Error processing progress data: {e}", exc_info=True) def _update_bambu_client_state(self, payload): """Aktualisiert die internen Zustände des BambuClient""" diff --git a/octoprint_bambu_printer/printer/file_system/cached_file_view.py b/octoprint_bambu_printer/printer/file_system/cached_file_view.py index 8d1d4d8..14af742 100644 --- a/octoprint_bambu_printer/printer/file_system/cached_file_view.py +++ b/octoprint_bambu_printer/printer/file_system/cached_file_view.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, List, Optional if TYPE_CHECKING: from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( @@ -12,83 +12,59 @@ from pathlib import Path from octoprint_bambu_printer.printer.file_system.file_info import FileInfo -@dataclass class CachedFileView: - file_system: RemoteSDCardFileList - folder_view: dict[tuple[str, str | list[str] | None], None] = field( - default_factory=dict - ) # dict preserves order, but set does not. We use only dict keys as storage - on_update: Callable[[], None] | None = None + def __init__( + self, file_system, on_update: Optional[Callable] = None, base_path: str = "" + ): + self._filters = [] + self._file_system = file_system + self._base_path = base_path + self._update_complete_callback = on_update + self._file_info_cache = [] - def __post_init__(self): - self._file_alias_cache: dict[str, str] = {} - self._file_data_cache: dict[str, FileInfo] = {} - - def with_filter( - self, folder: str, extensions: str | list[str] | None = None - ) -> "CachedFileView": - self.folder_view[(folder, extensions)] = None + def with_filter(self, path: str, extension: str): + self._filters.append({"path": path, "extension": extension}) return self - def list_all_views(self): - existing_files: list[str] = [] - result: list[FileInfo] = [] + def update(self) -> None: + try: + file_info_list = self.list_all_views() + self._file_info_cache = file_info_list + + # Rufe Callback auf, wenn vorhanden + if self._update_complete_callback is not None: + self._update_complete_callback() + except Exception as e: + import logging + logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter").error( + f"Error updating file list: {e}", exc_info=True + ) - with self.file_system.get_ftps_client() as ftp: - for filter in self.folder_view.keys(): - result.extend(self.file_system.list_files(*filter, ftp, existing_files)) - return result + def list_all_views(self) -> List[FileInfo]: + # Verwende die Mock-Implementation von get_file_list statt FTPS + try: + return self._file_system.get_file_list(self._base_path) + except Exception as e: + import logging + logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter").error( + f"Error listing files: {e}", exc_info=True + ) + return [] - def update(self): - file_info_list = self.list_all_views() - self._update_file_list_cache(file_info_list) - if self.on_update: - self.on_update() + def get_all_cached_info(self) -> List[FileInfo]: + return self._file_info_cache - def _update_file_list_cache(self, files: list[FileInfo]): - self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files} - self._file_data_cache = {info.path.as_posix(): info for info in files} + def get_file_by_stem(self, file_stem: str, extensions: list[str]) -> FileInfo | None: + """Get file info by file name without extension""" + for file_info in self._file_info_cache: + for extension in extensions: + if file_info.file_name.lower().startswith(f"{file_stem.lower()}{extension}"): + return file_info - def get_all_info(self): - self.update() - return self.get_all_cached_info() - - def get_all_cached_info(self): - return list(self._file_data_cache.values()) - - def get_file_data(self, file_path: str | Path) -> FileInfo | None: - file_data = self.get_file_data_cached(file_path) - if file_data is None: - self.update() - file_data = self.get_file_data_cached(file_path) - return file_data - - def get_file_data_cached(self, file_path: str | Path) -> FileInfo | None: - if isinstance(file_path, str): - file_path = Path(file_path).as_posix().strip("/") - else: - file_path = file_path.as_posix().strip("/") - - if file_path not in self._file_data_cache: - file_path = self._file_alias_cache.get(file_path, file_path) - return self._file_data_cache.get(file_path, None) - - def get_file_by_stem(self, file_stem: str, allowed_suffixes: list[str]): - if file_stem == "": - return None - - file_stem = Path(file_stem).with_suffix("").stem - file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) - if file_data is None: - self.update() - file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) - return file_data - - def _get_file_by_stem_cached(self, file_stem: str, allowed_suffixes: list[str]): - for file_path_str in list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()): - file_path = Path(file_path_str) - if file_stem == file_path.with_suffix("").stem and all( - suffix in allowed_suffixes for suffix in file_path.suffixes - ): - return self.get_file_data_cached(file_path) + return None + + def get_file_data(self, file_path: str) -> FileInfo | None: + for file_info in self._file_info_cache: + if file_info.path.lower() == file_path.lower() or file_info.file_name.lower() == file_path.lower(): + return file_info return None diff --git a/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py b/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py index b6bd14d..fe76d8e 100644 --- a/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py +++ b/octoprint_bambu_printer/printer/file_system/remote_sd_card_file_list.py @@ -2,7 +2,7 @@ from __future__ import annotations import datetime from pathlib import Path -from typing import Iterable, Iterator +from typing import Iterable, Iterator, List import logging.handlers from octoprint.util import get_dos_filename @@ -17,6 +17,7 @@ class RemoteSDCardFileList: self._settings = settings self._selected_project_file: FileInfo | None = None self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") + self._mock_files = [] # Lokales Cache für Mock-Dateien def delete_file(self, file_path: Path) -> None: try: @@ -80,8 +81,56 @@ class RemoteSDCardFileList: self._logger.exception(e, exc_info=False) def get_ftps_client(self): - host = self._settings.get(["host"]) - access_code = self._settings.get(["access_code"]) - return IoTFTPSClient( - f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True - ) + """ + Implementieren wir eine Mock-Version des FTPS-Clients, die keinen echten FTP-Zugriff erfordert. + """ + class MockFTPSClient: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def get_file_list(self, path=""): + """Gibt die Mock-Dateiliste zurück""" + return self._mock_files + + mock_client = MockFTPSClient() + mock_client._mock_files = self._mock_files + return mock_client + + @property + def is_available(self) -> bool: + """ + Da wir kein FTP verwenden, ist dieser Service immer verfügbar + """ + return True + + def get_file_list(self, path: str) -> List[FileInfo]: + """ + Gibt eine Liste von Dateien im angegebenen Pfad zurück. + Da wir kein FTP verwenden, geben wir eine leere Liste oder gespeicherte Mock-Dateien zurück. + """ + self._logger.debug(f"Listing files in path: {path}") + return self._mock_files + + def add_mock_file(self, file_info: FileInfo): + """ + Fügt eine Mock-Datei zur Liste hinzu (für Tests oder wenn keine FTP-Verbindung möglich ist) + """ + self._mock_files.append(file_info) + self._logger.debug(f"Added mock file: {file_info.file_name}") + + def clear_mock_files(self): + """Löscht alle gespeicherten Mock-Dateien""" + self._mock_files = [] + self._logger.debug("Mock file list cleared") + + def delete_file(self, path: str) -> bool: + """ + Simuliert das Löschen einer Datei, entfernt sie aus der Mock-Liste + """ + self._logger.debug(f"Deleting file: {path}") + before_count = len(self._mock_files) + self._mock_files = [f for f in self._mock_files if f.path != path] + return before_count > len(self._mock_files)