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:
# Normalisieren des Status, falls er 'unknown' ist progress = int(print_data['mc_percent'])
if print_job_state == "unknown":
# Wenn der Status unbekannt ist, versuchen wir, ihn aus anderen Informationen abzuleiten remaining_time = 0
if hasattr(self._bambu_client, 'device') and hasattr(self._bambu_client.device, 'print_job'): if 'mc_remaining_time' in print_data:
# Prüfe ob Druckfortschritt vorhanden ist remaining_time = int(print_data['mc_remaining_time'])
if hasattr(self._bambu_client.device.print_job, 'mc_percent') and self._bambu_client.device.print_job.mc_percent > 0:
print_job_state = "RUNNING" # Aktualisiere den PrintJob, wenn einer existiert
self._log.debug(f"Changed unknown state to RUNNING based on print progress") if self.current_print_job is not None:
self.current_print_job.print_percentage = progress
if print_job_state in ["IDLE", "FINISH", "FAILED"]: self.current_print_job.remaining_time = remaining_time
self.change_state(self._state_idle) self._log.debug(f"Updated print progress: {progress}%, remaining: {remaining_time}s")
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}")
# 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] = [] 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: def list_all_views(self) -> List[FileInfo]:
for filter in self.folder_view.keys(): # Verwende die Mock-Implementation von get_file_list statt FTPS
result.extend(self.file_system.list_files(*filter, ftp, existing_files)) try:
return result 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): def get_all_cached_info(self) -> List[FileInfo]:
file_info_list = self.list_all_views() return self._file_info_cache
self._update_file_list_cache(file_info_list)
if self.on_update:
self.on_update()
def _update_file_list_cache(self, files: list[FileInfo]): def get_file_by_stem(self, file_stem: str, extensions: list[str]) -> FileInfo | None:
self._file_alias_cache = {info.dosname: info.path.as_posix() for info in files} """Get file info by file name without extension"""
self._file_data_cache = {info.path.as_posix(): info for info in files} 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): return None
self.update()
return self.get_all_cached_info() def get_file_data(self, file_path: str) -> FileInfo | None:
for file_info in self._file_info_cache:
def get_all_cached_info(self): if file_info.path.lower() == file_path.lower() or file_info.file_name.lower() == file_path.lower():
return list(self._file_data_cache.values()) return file_info
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 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)