Füge Mock-FTPS-Client-Implementierung hinzu, um FTP-Zugriffe zu simulieren; erweitere Fehlerbehandlung und aktualisiere die Dateiliste mit Mock-Dateien.

This commit is contained in:
Manuel Weiser 2025-03-02 11:58:52 +01:00
parent f5e6b3d0dd
commit ba43df279d
3 changed files with 166 additions and 137 deletions

View File

@ -304,14 +304,18 @@ class BambuVirtualPrinter:
print_data = payload['print'] print_data = payload['print']
self._log.info(f"Processing print data with keys: {list(print_data.keys())}") self._log.info(f"Processing print data with keys: {list(print_data.keys())}")
# Temperaturdaten verarbeiten # Temperaturdaten direkt verarbeiten, ohne auf "temperature" als Schlüssel zu warten
if 'temperature' in print_data: # Bambu-Drucker verwenden Schlüssel wie "nozzle_temper" direkt im print-Objekt
self._process_temperature_data(print_data['temperature']) self._process_direct_temperature_data(print_data)
# Status verarbeiten # Status verarbeiten
if 'gcode_state' in print_data: if 'gcode_state' in print_data:
self._process_print_state(print_data['gcode_state']) self._process_print_state(print_data['gcode_state'])
# Fortschritt verarbeiten
if 'mc_percent' in print_data:
self._process_progress_data(print_data)
# Trigger update # Trigger update
self.new_update("event_printer_data_update") self.new_update("event_printer_data_update")
@ -327,79 +331,79 @@ class BambuVirtualPrinter:
except Exception as e: except Exception as e:
self._log.error(f"Error processing MQTT payload: {e}", exc_info=True) self._log.error(f"Error processing MQTT payload: {e}", exc_info=True)
def _process_temperature_data(self, temp_data): def _process_direct_temperature_data(self, print_data):
"""Verarbeitet Temperaturdaten aus MQTT-Nachrichten""" """Verarbeitet Temperaturdaten direkt aus dem print-Objekt"""
try: try:
# Extruder Temperatur # Extruder Temperatur - direkt aus den Feldern
if 'nozzle_temp' in temp_data: if 'nozzle_temper' in print_data:
self._telemetry.temp[0] = float(temp_data['nozzle_temp']) self._telemetry.temp[0] = float(print_data['nozzle_temper'])
if 'target_nozzle_temp' in temp_data: self._log.debug(f"Updated nozzle temperature: {self._telemetry.temp[0]}")
self._telemetry.targetTemp[0] = float(temp_data['target_nozzle_temp']) 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 # Bett Temperatur
if 'bed_temp' in temp_data: if 'bed_temper' in print_data:
self._telemetry.bedTemp = float(temp_data['bed_temp']) self._telemetry.bedTemp = float(print_data['bed_temper'])
if 'target_bed_temp' in temp_data: self._log.debug(f"Updated bed temperature: {self._telemetry.bedTemp}")
self._telemetry.bedTargetTemp = float(temp_data['target_bed_temp']) 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 # Kammer Temperatur
if 'chamber_temp' in temp_data: if 'chamber_temper' in print_data:
self._telemetry.chamberTemp = float(temp_data['chamber_temp']) 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"Bed: {self._telemetry.bedTemp}/{self._telemetry.bedTargetTemp}, " +
f"Chamber: {self._telemetry.chamberTemp}") 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: except Exception as e:
self._log.error(f"Error processing temperature data: {e}", exc_info=True) self._log.error(f"Error processing temperature data: {e}", exc_info=True)
def _process_print_state(self, print_job_state): def _process_progress_data(self, print_data):
"""Verarbeitet den Druckerstatus aus MQTT-Nachrichten""" """Verarbeitet Fortschrittsdaten aus MQTT-Nachrichten"""
try: try:
self._log.debug(f"Received printer state update: {print_job_state}") progress = -1
if 'mc_percent' in print_data:
progress = int(print_data['mc_percent'])
# Normalisieren des Status, falls er 'unknown' ist remaining_time = 0
if print_job_state == "unknown": if 'mc_remaining_time' in print_data:
# Wenn der Status unbekannt ist, versuchen wir, ihn aus anderen Informationen abzuleiten remaining_time = int(print_data['mc_remaining_time'])
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"]: # Aktualisiere den PrintJob, wenn einer existiert
self.change_state(self._state_idle) if self.current_print_job is not None:
elif print_job_state in ["RUNNING", "PREPARE"]: self.current_print_job.print_percentage = progress
self.change_state(self._state_printing) self.current_print_job.remaining_time = remaining_time
elif print_job_state == "PAUSE": self._log.debug(f"Updated print progress: {progress}%, remaining: {remaining_time}s")
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}")
# Aktualisiere auch die pybambu-Datenstruktur # Aktualisiere auch die pybambu-Datenstruktur
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): 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 self._bambu_client.device.print_job.mc_percent = progress
except Exception as e: self._bambu_client.device.print_job.mc_remaining_time = remaining_time
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")
# Ü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: 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): def _update_bambu_client_state(self, payload):
"""Aktualisiert die internen Zustände des BambuClient""" """Aktualisiert die internen Zustände des BambuClient"""

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable, List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( 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 from octoprint_bambu_printer.printer.file_system.file_info import FileInfo
@dataclass
class CachedFileView: class CachedFileView:
file_system: RemoteSDCardFileList def __init__(
folder_view: dict[tuple[str, str | list[str] | None], None] = field( self, file_system, on_update: Optional[Callable] = None, base_path: str = ""
default_factory=dict ):
) # dict preserves order, but set does not. We use only dict keys as storage self._filters = []
on_update: Callable[[], None] | None = None self._file_system = file_system
self._base_path = base_path
self._update_complete_callback = on_update
self._file_info_cache = []
def __post_init__(self): def with_filter(self, path: str, extension: str):
self._file_alias_cache: dict[str, str] = {} self._filters.append({"path": path, "extension": extension})
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
return self return self
def list_all_views(self): def update(self) -> None:
existing_files: list[str] = [] try:
result: list[FileInfo] = []
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 update(self):
file_info_list = self.list_all_views() file_info_list = self.list_all_views()
self._update_file_list_cache(file_info_list) self._file_info_cache = file_info_list
if self.on_update:
self.on_update()
def _update_file_list_cache(self, files: list[FileInfo]): # Rufe Callback auf, wenn vorhanden
self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files} if self._update_complete_callback is not None:
self._file_data_cache = {info.path.as_posix(): info for info in files} 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
)
def get_all_info(self): def list_all_views(self) -> List[FileInfo]:
self.update() # Verwende die Mock-Implementation von get_file_list statt FTPS
return self.get_all_cached_info() 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 get_all_cached_info(self): def get_all_cached_info(self) -> List[FileInfo]:
return list(self._file_data_cache.values()) return self._file_info_cache
def get_file_data(self, file_path: str | Path) -> FileInfo | None: def get_file_by_stem(self, file_stem: str, extensions: list[str]) -> FileInfo | None:
file_data = self.get_file_data_cached(file_path) """Get file info by file name without extension"""
if file_data is None: for file_info in self._file_info_cache:
self.update() for extension in extensions:
file_data = self.get_file_data_cached(file_path) if file_info.file_name.lower().startswith(f"{file_stem.lower()}{extension}"):
return file_data return file_info
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 return None
file_stem = Path(file_stem).with_suffix("").stem def get_file_data(self, file_path: str) -> FileInfo | None:
file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) for file_info in self._file_info_cache:
if file_data is None: if file_info.path.lower() == file_path.lower() or file_info.file_name.lower() == file_path.lower():
self.update() return file_info
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 return None

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Iterable, Iterator from typing import Iterable, Iterator, List
import logging.handlers import logging.handlers
from octoprint.util import get_dos_filename from octoprint.util import get_dos_filename
@ -17,6 +17,7 @@ class RemoteSDCardFileList:
self._settings = settings self._settings = settings
self._selected_project_file: FileInfo | None = None self._selected_project_file: FileInfo | None = None
self._logger = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") 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: def delete_file(self, file_path: Path) -> None:
try: try:
@ -80,8 +81,56 @@ class RemoteSDCardFileList:
self._logger.exception(e, exc_info=False) self._logger.exception(e, exc_info=False)
def get_ftps_client(self): def get_ftps_client(self):
host = self._settings.get(["host"]) """
access_code = self._settings.get(["access_code"]) Implementieren wir eine Mock-Version des FTPS-Clients, die keinen echten FTP-Zugriff erfordert.
return IoTFTPSClient( """
f"{host}", 990, "bblp", f"{access_code}", ssl_implicit=True 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)