Compare commits
	
		
			21 Commits
		
	
	
		
			0.1.8rc12
			...
			fd9ce76275
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fd9ce76275 | |||
| 8dafb9fa5a | |||
| 094959335a | |||
| f64fa7aea2 | |||
| fea0f0ed25 | |||
| c7c089ef68 | |||
| ba43df279d | |||
| f5e6b3d0dd | |||
| 9358533ce8 | |||
| 92e11cdbf3 | |||
| 61c9332f15 | |||
| ad08d3eb9a | |||
| 5661c11190 | |||
| 3690767ced | |||
| eb397ff7b7 | |||
| 3a615cfafe | |||
| e9c06bb4b5 | |||
| 3ccce10648 | |||
| c99eb38655 | |||
| 698f8f4151 | |||
| 7a0293bac7 | 
							
								
								
									
										21
									
								
								.github/workflows/issue-validator.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/issue-validator.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,21 +0,0 @@ | ||||
| name: issue validator | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   issues: | ||||
|     types: [opened, edited] | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|  | ||||
| jobs: | ||||
|   validate: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: Okabe-Junya/issue-validator@v0.4.1 | ||||
|         with: | ||||
|           body: '/\[(octoprint\.log)\]|\[(plugin_bambu_printer_serial\.log)\]/g' | ||||
|           body-regex-flags: 'true' | ||||
|           is-auto-close: 'true' | ||||
|           issue-type: 'both' | ||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
| @@ -2,4 +2,3 @@ include README.md | ||||
| recursive-include octoprint_bambu_printer/templates * | ||||
| recursive-include octoprint_bambu_printer/translations * | ||||
| recursive-include octoprint_bambu_printer/static * | ||||
| include octoprint_bambu_printer/printer/pybambu/filaments.json | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| from __future__ import absolute_import, annotations | ||||
|  | ||||
| import json | ||||
| from pathlib import Path | ||||
| import threading | ||||
| from time import perf_counter | ||||
| @@ -8,8 +6,6 @@ from contextlib import contextmanager | ||||
| import flask | ||||
| import logging.handlers | ||||
| from urllib.parse import quote as urlquote | ||||
| import os | ||||
| import zipfile | ||||
|  | ||||
| import octoprint.printer | ||||
| import octoprint.server | ||||
| @@ -26,7 +22,7 @@ from octoprint.access.permissions import Permissions | ||||
| from octoprint.logging.handlers import CleaningTimedRotatingFileHandler | ||||
|  | ||||
| from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | ||||
| from octoprint_bambu_printer.printer.pybambu import BambuCloud | ||||
| from pybambu import BambuCloud | ||||
|  | ||||
| from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( | ||||
|     RemoteSDCardFileList, | ||||
| @@ -61,7 +57,6 @@ class BambuPrintPlugin( | ||||
|     _plugin_manager: octoprint.plugin.PluginManager | ||||
|     _bambu_file_system: RemoteSDCardFileList | ||||
|     _timelapse_files_view: CachedFileView | ||||
|     _bambu_cloud: None | ||||
|  | ||||
|     def on_settings_initialized(self): | ||||
|         self._bambu_file_system = RemoteSDCardFileList(self._settings) | ||||
| @@ -72,9 +67,7 @@ class BambuPrintPlugin( | ||||
|             self._timelapse_files_view.with_filter("timelapse/", ".avi") | ||||
|  | ||||
|     def get_assets(self): | ||||
|         return {"js": ["js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/bambu_printer.js"], | ||||
|                 "css": ["css/bambu_printer.css"] | ||||
|                 } | ||||
|         return {"js": ["js/bambu_printer.js"]} | ||||
|  | ||||
|     def get_template_configs(self): | ||||
|         return [ | ||||
| @@ -84,7 +77,7 @@ class BambuPrintPlugin( | ||||
|                 "custom_bindings": True, | ||||
|                 "template": "bambu_timelapse.jinja2", | ||||
|             }, | ||||
|             {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}] | ||||
|         ]  # , {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}] | ||||
|  | ||||
|     def get_settings_defaults(self): | ||||
|         return { | ||||
| @@ -92,7 +85,7 @@ class BambuPrintPlugin( | ||||
|             "serial": "", | ||||
|             "host": "", | ||||
|             "access_code": "", | ||||
|             "username": "bblp", | ||||
|             "username": "octobambu", | ||||
|             "timelapse": False, | ||||
|             "bed_leveling": True, | ||||
|             "flow_cali": False, | ||||
| @@ -104,22 +97,13 @@ class BambuPrintPlugin( | ||||
|             "email": "", | ||||
|             "auth_token": "", | ||||
|             "always_use_default_options": False, | ||||
|             "ams_data": [], | ||||
|             "ams_mapping": [], | ||||
|             "ams_current_tray": 255, | ||||
|         } | ||||
|  | ||||
|     def on_settings_save(self, data): | ||||
|         if data.get("local_mqtt", False) is True: | ||||
|             data["auth_token"] = "" | ||||
|         octoprint.plugin.SettingsPlugin.on_settings_save(self, data) | ||||
|  | ||||
|     def is_api_adminonly(self): | ||||
|         return True | ||||
|  | ||||
|     def get_api_commands(self): | ||||
|         return {"register": ["email", "password", "region", "auth_token"], | ||||
|                 "verify": ["auth_type", "password"]} | ||||
|         return {"register": ["email", "password", "region", "auth_token"]} | ||||
|  | ||||
|     def on_api_command(self, command, data): | ||||
|         if command == "register": | ||||
| @@ -130,57 +114,20 @@ class BambuPrintPlugin( | ||||
|                 and "auth_token" in data | ||||
|             ): | ||||
|                 self._logger.info(f"Registering user {data['email']}") | ||||
|                 self._bambu_cloud = BambuCloud( | ||||
|                 bambu_cloud = BambuCloud( | ||||
|                     data["region"], data["email"], data["password"], data["auth_token"] | ||||
|                 ) | ||||
|                 auth_response = self._bambu_cloud.login(data["region"], data["email"], data["password"]) | ||||
|                 bambu_cloud.login(data["region"], data["email"], data["password"]) | ||||
|                 return flask.jsonify( | ||||
|                     { | ||||
|                         "auth_response": auth_response, | ||||
|                     } | ||||
|                 ) | ||||
|         elif command == "verify": | ||||
|             auth_response = None | ||||
|             if ( | ||||
|                 "auth_type" in data | ||||
|                 and "password" in data | ||||
|                 and self._bambu_cloud is not None | ||||
|             ): | ||||
|                 self._logger.info(f"Verifying user {self._bambu_cloud._email}") | ||||
|                 if data["auth_type"] == "verifyCode": | ||||
|                     auth_response = self._bambu_cloud.login_with_verification_code(data["password"]) | ||||
|                 elif data["auth_type"] == "tfa": | ||||
|                     auth_response = self._bambu_cloud.login_with_2fa_code(data["password"]) | ||||
|                 else: | ||||
|                     self._logger.warning(f"Unknown verification type: {data['auth_type']}") | ||||
|  | ||||
|                 if auth_response == "success": | ||||
|                     return flask.jsonify( | ||||
|                         { | ||||
|                             "auth_token": self._bambu_cloud.auth_token, | ||||
|                             "username": self._bambu_cloud.username | ||||
|                         } | ||||
|                     ) | ||||
|                 else: | ||||
|                     self._logger.info(f"Error verifying: {auth_response}") | ||||
|                     return flask.jsonify( | ||||
|                         { | ||||
|                             "error": "Unable to verify" | ||||
|                         "auth_token": bambu_cloud.auth_token, | ||||
|                         "username": bambu_cloud.username, | ||||
|                     } | ||||
|                 ) | ||||
|  | ||||
|     def on_event(self, event, payload): | ||||
|         if event == Events.TRANSFER_DONE: | ||||
|             self._printer.commands("M20 L T", force=True) | ||||
|         elif event == Events.FILE_ADDED: | ||||
|             if payload["operation"] == "add" and "3mf" in payload["type"]: | ||||
|                 file_container = os.path.join(self._settings.getBaseFolder("uploads"), payload["path"]) | ||||
|                 with zipfile.ZipFile(file_container) as z: | ||||
|                     with z.open("Metadata/plate_1.json", "r") as json_data: | ||||
|                         plate_data = json.load(json_data) | ||||
|  | ||||
|                 if plate_data: | ||||
|                     self._file_manager.set_additional_metadata("sdcard", payload["path"], "plate_data", plate_data, overwrite=True) | ||||
|  | ||||
|     def support_3mf_files(self): | ||||
|         return {"machinecode": {"3mf": ["3mf"]}} | ||||
| @@ -339,10 +286,10 @@ class BambuPrintPlugin( | ||||
|     def get_update_information(self): | ||||
|         return { | ||||
|             "bambu_printer": { | ||||
|                 "displayName": "Bambu Printer", | ||||
|                 "displayName": "Manus Bambu Printer", | ||||
|                 "displayVersion": self._plugin_version, | ||||
|                 "type": "github_release", | ||||
|                 "user": "jneilliii", | ||||
|                 "user": "ManuelW", | ||||
|                 "repo": "OctoPrint-BambuPrinter", | ||||
|                 "current": self._plugin_version, | ||||
|                 "stable_branch": { | ||||
| @@ -357,6 +304,6 @@ class BambuPrintPlugin( | ||||
|                         "comittish": ["rc", "master"], | ||||
|                     } | ||||
|                 ], | ||||
|                 "pip": "https://github.com/jneilliii/OctoPrint-BambuPrinter/archive/{target_version}.zip", | ||||
|                 "pip": "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter/archive/{target_version}.zip", | ||||
|             } | ||||
|         } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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,87 +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] = [] | ||||
|  | ||||
|         with self.file_system.get_ftps_client() as ftp: | ||||
|             for key in self.folder_view.keys(): | ||||
|                 result.extend(self.file_system.list_files(*key, ftp, existing_files)) | ||||
|         return result | ||||
|  | ||||
|     def update(self): | ||||
|     def update(self) -> None: | ||||
|         try: | ||||
|             file_info_list = self.list_all_views() | ||||
|         self._update_file_list_cache(file_info_list) | ||||
|         if self.on_update: | ||||
|             self.on_update() | ||||
|             self._file_info_cache = file_info_list | ||||
|              | ||||
|     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} | ||||
|             # 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 | ||||
|             ) | ||||
|  | ||||
|     def get_all_info(self): | ||||
|         self.update() | ||||
|         return self.get_all_cached_info() | ||||
|     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 get_all_cached_info(self): | ||||
|         return list(self._file_data_cache.values()) | ||||
|     def get_all_cached_info(self) -> List[FileInfo]: | ||||
|         return self._file_info_cache | ||||
|  | ||||
|     def get_keys_as_list(self): | ||||
|         return list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()) | ||||
|     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_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_name(self, file_name: str): | ||||
|         if file_name == "": | ||||
|         return None | ||||
|  | ||||
|         file_list = self.get_keys_as_list() | ||||
|         if not file_name in file_list: | ||||
|             if f"{file_name}.3mf" in file_list: | ||||
|                 file_name = f"{file_name}.3mf" | ||||
|             elif f"{file_name}.gcode.3mf" in file_list: | ||||
|                 file_name = f"{file_name}.gcode.3mf" | ||||
|             elif f"cache/{file_name}.3mf" in file_list: | ||||
|                 file_name = f"cache/{file_name}.3mf" | ||||
|             elif f"cache/{file_name}.gcode.3mf" in file_list: | ||||
|                 file_name = f"cache/{file_name}.gcode.3mf" | ||||
|  | ||||
|         file_data = self.get_file_data_cached(file_name) | ||||
|         if file_data is None: | ||||
|             self.update() | ||||
|             return self.get_file_by_name(file_name) | ||||
|         return file_data | ||||
|     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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -10,9 +10,6 @@ from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import | ||||
| class PrintJob: | ||||
|     file_info: FileInfo | ||||
|     progress: int | ||||
|     remaining_time: int | ||||
|     current_layer: int | ||||
|     total_layers: int | ||||
|  | ||||
|     @property | ||||
|     def file_position(self): | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| """Initialise the Bambu Client""" | ||||
| # TODO: Once complete, move pybambu to PyPi | ||||
| from .bambu_client import BambuClient | ||||
| from .bambu_cloud  import BambuCloud | ||||
| @@ -1,594 +0,0 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import queue | ||||
| import json | ||||
| import math | ||||
| import re | ||||
| import socket | ||||
| import ssl | ||||
| import struct | ||||
| import threading | ||||
| import time | ||||
|  | ||||
| from dataclasses import dataclass | ||||
| from typing import Any | ||||
|  | ||||
| import paho.mqtt.client as mqtt | ||||
|  | ||||
| from .bambu_cloud import BambuCloud | ||||
| from .const import ( | ||||
|     LOGGER, | ||||
|     Features, | ||||
| ) | ||||
| from .models import Device, SlicerSettings | ||||
| from .commands import ( | ||||
|     GET_VERSION, | ||||
|     PUSH_ALL, | ||||
|     START_PUSH, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class WatchdogThread(threading.Thread): | ||||
|  | ||||
|     def __init__(self, client): | ||||
|         self._client = client | ||||
|         self._watchdog_fired = False | ||||
|         self._stop_event = threading.Event() | ||||
|         self._last_received_data = time.time() | ||||
|         super().__init__() | ||||
|         self.daemon = True | ||||
|         self.setName(f"{self._client._device.info.device_type}-Watchdog-{threading.get_native_id()}") | ||||
|  | ||||
|     def stop(self): | ||||
|         self._stop_event.set() | ||||
|  | ||||
|     def received_data(self): | ||||
|         self._last_received_data = time.time() | ||||
|  | ||||
|     def run(self): | ||||
|         LOGGER.info("Watchdog thread started.") | ||||
|         WATCHDOG_TIMER = 30 | ||||
|         while True: | ||||
|             # Wait out the remainder of the watchdog delay or 1s, whichever is higher. | ||||
|             interval = time.time() - self._last_received_data | ||||
|             wait_time = max(1, WATCHDOG_TIMER - interval) | ||||
|             if self._stop_event.wait(wait_time): | ||||
|                 # Stop event has been set. Exit thread. | ||||
|                 break | ||||
|             interval = time.time() - self._last_received_data | ||||
|             if not self._watchdog_fired and (interval > WATCHDOG_TIMER): | ||||
|                 LOGGER.debug(f"Watchdog fired. No data received for {math.floor(interval)} seconds for {self._client._serial}.") | ||||
|                 self._watchdog_fired = True | ||||
|                 self._client._on_watchdog_fired() | ||||
|             elif interval < WATCHDOG_TIMER: | ||||
|                 self._watchdog_fired = False | ||||
|  | ||||
|         LOGGER.info("Watchdog thread exited.") | ||||
|  | ||||
|  | ||||
| class ChamberImageThread(threading.Thread): | ||||
|     def __init__(self, client): | ||||
|         self._client = client | ||||
|         self._stop_event = threading.Event() | ||||
|         super().__init__() | ||||
|         self.daemon = True | ||||
|         self.setName(f"{self._client._device.info.device_type}-Chamber-{threading.get_native_id()}") | ||||
|  | ||||
|     def stop(self): | ||||
|         self._stop_event.set() | ||||
|  | ||||
|     def run(self): | ||||
|         LOGGER.debug("Chamber image thread started.") | ||||
|  | ||||
|         auth_data = bytearray() | ||||
|  | ||||
|         username = 'bblp' | ||||
|         access_code = self._client._access_code | ||||
|         hostname = self._client.host | ||||
|         port = 6000 | ||||
|         MAX_CONNECT_ATTEMPTS = 12 | ||||
|         connect_attempts = 0 | ||||
|  | ||||
|         auth_data += struct.pack("<I", 0x40)   # '@'\0\0\0 | ||||
|         auth_data += struct.pack("<I", 0x3000) # \0'0'\0\0 | ||||
|         auth_data += struct.pack("<I", 0)      # \0\0\0\0 | ||||
|         auth_data += struct.pack("<I", 0)      # \0\0\0\0 | ||||
|         for i in range(0, len(username)): | ||||
|             auth_data += struct.pack("<c", username[i].encode('ascii')) | ||||
|         for i in range(0, 32 - len(username)): | ||||
|             auth_data += struct.pack("<x") | ||||
|         for i in range(0, len(access_code)): | ||||
|             auth_data += struct.pack("<c", access_code[i].encode('ascii')) | ||||
|         for i in range(0, 32 - len(access_code)): | ||||
|             auth_data += struct.pack("<x") | ||||
|  | ||||
|         ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||||
|         ctx.check_hostname = False | ||||
|         ctx.verify_mode = ssl.CERT_NONE | ||||
|  | ||||
|         jpeg_start = bytearray([0xff, 0xd8, 0xff, 0xe0]) | ||||
|         jpeg_end = bytearray([0xff, 0xd9]) | ||||
|  | ||||
|         read_chunk_size = 4096 # 4096 is the max we'll get even if we increase this. | ||||
|  | ||||
|         # Payload format for each image is: | ||||
|         # 16 byte header: | ||||
|         #   Bytes 0:3   = little endian payload size for the jpeg image (does not include this header). | ||||
|         #   Bytes 4:7   = 0x00000000 | ||||
|         #   Bytes 8:11  = 0x00000001 | ||||
|         #   Bytes 12:15 = 0x00000000 | ||||
|         # These first 16 bytes are always delivered by themselves. | ||||
|         # | ||||
|         # Bytes 16:19                       = jpeg_start magic bytes | ||||
|         # Bytes 20:payload_size-2           = jpeg image bytes | ||||
|         # Bytes payload_size-2:payload_size = jpeg_end magic bytes | ||||
|         # | ||||
|         # Further attempts to receive data will get SSLWantReadError until a new image is ready (1-2 seconds later) | ||||
|         while connect_attempts < MAX_CONNECT_ATTEMPTS and not self._stop_event.is_set(): | ||||
|             connect_attempts += 1 | ||||
|             try: | ||||
|                 with socket.create_connection((hostname, port)) as sock: | ||||
|                     try: | ||||
|                         sslSock = ctx.wrap_socket(sock, server_hostname=hostname) | ||||
|                         sslSock.write(auth_data) | ||||
|                         img = None | ||||
|                         payload_size = 0 | ||||
|  | ||||
|                         status = sslSock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) | ||||
|                         LOGGER.debug(f"SOCKET STATUS: {status}") | ||||
|                         if status != 0: | ||||
|                             LOGGER.error(f"Socket error: {status}") | ||||
|                     except socket.error as e: | ||||
|                         LOGGER.error(f"Socket error: {e}") | ||||
|                         # Sleep to allow printer to stabilize during boot when it may fail these connection attempts repeatedly. | ||||
|                         time.sleep(1) | ||||
|                         continue | ||||
|  | ||||
|                     sslSock.setblocking(False) | ||||
|                     while not self._stop_event.is_set(): | ||||
|                         try: | ||||
|                             dr = sslSock.recv(read_chunk_size) | ||||
|                             #LOGGER.debug(f"Received {len(dr)} bytes.") | ||||
|  | ||||
|                         except ssl.SSLWantReadError: | ||||
|                             #LOGGER.debug("SSLWantReadError") | ||||
|                             time.sleep(1) | ||||
|                             continue | ||||
|  | ||||
|                         except Exception as e: | ||||
|                             LOGGER.error("A Chamber Image thread inner exception occurred:") | ||||
|                             LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") | ||||
|                             time.sleep(1) | ||||
|                             continue | ||||
|  | ||||
|                         if img is not None and len(dr) > 0: | ||||
|                             img += dr | ||||
|                             if len(img) > payload_size: | ||||
|                                 # We got more data than we expected. | ||||
|                                 LOGGER.error(f"Unexpected image payload received: {len(img)} > {payload_size}") | ||||
|                                 # Reset buffer | ||||
|                                 img = None | ||||
|                             elif len(img) == payload_size: | ||||
|                                 # We should have the full image now. | ||||
|                                 if img[:4] != jpeg_start: | ||||
|                                     LOGGER.error("JPEG start magic bytes missing.") | ||||
|                                 elif img[-2:] != jpeg_end: | ||||
|                                     LOGGER.error("JPEG end magic bytes missing.") | ||||
|                                 else: | ||||
|                                     # Content is as expected. Send it. | ||||
|                                     self._client.on_jpeg_received(img) | ||||
|  | ||||
|                                 # Reset buffer | ||||
|                                 img = None | ||||
|                             # else:      | ||||
|                             # Otherwise we need to continue looping without reseting the buffer to receive the remaining data | ||||
|                             # and without delaying. | ||||
|  | ||||
|                         elif len(dr) == 16: | ||||
|                             # We got the header bytes. Get the expected payload size from it and create the image buffer bytearray. | ||||
|                             # Reset connect_attempts now we know the connect was successful. | ||||
|                             connect_attempts = 0 | ||||
|                             img = bytearray() | ||||
|                             payload_size = int.from_bytes(dr[0:3], byteorder='little') | ||||
|  | ||||
|                         elif len(dr) == 0: | ||||
|                             # This occurs if the wrong access code was provided. | ||||
|                             LOGGER.error("Chamber image connection rejected by the printer. Check provided access code and IP address.") | ||||
|                             # Sleep for a short while and then re-attempt the connection. | ||||
|                             time.sleep(5) | ||||
|                             break | ||||
|  | ||||
|                         else: | ||||
|                             LOGGER.error(f"UNEXPECTED DATA RECEIVED: {len(dr)}") | ||||
|                             time.sleep(1) | ||||
|  | ||||
|             except OSError as e: | ||||
|                 if e.errno == 113: | ||||
|                     LOGGER.debug("Host is unreachable") | ||||
|                 else: | ||||
|                     LOGGER.error("A Chamber Image thread outer exception occurred:") | ||||
|                     LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") | ||||
|                 if not self._stop_event.is_set(): | ||||
|                     time.sleep(1)  # Avoid a tight loop if this is a persistent error. | ||||
|  | ||||
|             except Exception as e: | ||||
|                 LOGGER.error(f"A Chamber Image thread outer exception occurred:") | ||||
|                 LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") | ||||
|                 if not self._stop_event.is_set(): | ||||
|                     time.sleep(1)  # Avoid a tight loop if this is a persistent error. | ||||
|  | ||||
|         LOGGER.debug("Chamber image thread exited.") | ||||
|  | ||||
|  | ||||
| class MqttThread(threading.Thread): | ||||
|     def __init__(self, client): | ||||
|         self._client = client | ||||
|         self._stop_event = threading.Event() | ||||
|         super().__init__() | ||||
|         self.daemon = True | ||||
|         self.setName(f"{self._client._device.info.device_type}-Mqtt-{threading.get_native_id()}") | ||||
|  | ||||
|     def stop(self): | ||||
|         self._stop_event.set() | ||||
|  | ||||
|     def run(self): | ||||
|         LOGGER.info("MQTT listener thread started.") | ||||
|         exceptionSeen = "" | ||||
|         while True: | ||||
|             try: | ||||
|                 host = self._client.host if self._client._local_mqtt else self._client.bambu_cloud.cloud_mqtt_host | ||||
|                 LOGGER.debug(f"Connect: Attempting Connection to {host}") | ||||
|                 self._client.client.connect(host, self._client._port, keepalive=5) | ||||
|  | ||||
|                 LOGGER.debug("Starting listen loop") | ||||
|                 self._client.client.loop_forever() | ||||
|                 LOGGER.debug("Ended listen loop.") | ||||
|                 break | ||||
|             except TimeoutError as e: | ||||
|                 if exceptionSeen != "TimeoutError": | ||||
|                     LOGGER.debug(f"TimeoutError: {e}.") | ||||
|                 exceptionSeen = "TimeoutError" | ||||
|                 time.sleep(5) | ||||
|             except ConnectionError as e: | ||||
|                 if exceptionSeen != "ConnectionError": | ||||
|                     LOGGER.debug(f"ConnectionError: {e}.") | ||||
|                 exceptionSeen = "ConnectionError" | ||||
|                 time.sleep(5) | ||||
|             except OSError as e: | ||||
|                 if e.errno == 113: | ||||
|                     if exceptionSeen != "OSError113": | ||||
|                         LOGGER.debug(f"OSError: {e}.") | ||||
|                     exceptionSeen = "OSError113" | ||||
|                     time.sleep(5) | ||||
|                 else: | ||||
|                     LOGGER.error("A listener loop thread exception occurred:") | ||||
|                     LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") | ||||
|                     time.sleep(1)  # Avoid a tight loop if this is a persistent error. | ||||
|             except Exception as e: | ||||
|                 LOGGER.error("A listener loop thread exception occurred:") | ||||
|                 LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") | ||||
|                 time.sleep(1)  # Avoid a tight loop if this is a persistent error. | ||||
|  | ||||
|             if self._client.client is None: | ||||
|                 break | ||||
|  | ||||
|             self._client.client.disconnect() | ||||
|  | ||||
|         LOGGER.info("MQTT listener thread exited.") | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class BambuClient: | ||||
|     """Initialize Bambu Client to connect to MQTT Broker""" | ||||
|     _watchdog = None | ||||
|     _camera = None | ||||
|     _usage_hours: float | ||||
|  | ||||
|     def __init__(self, config): | ||||
|         self.host = config['host'] | ||||
|         self._callback = None | ||||
|  | ||||
|         self._access_code = config.get('access_code', '') | ||||
|         self._auth_token = config.get('auth_token', '') | ||||
|         self._device_type = config.get('device_type', 'unknown') | ||||
|         self._local_mqtt = config.get('local_mqtt', False) | ||||
|         self._manual_refresh_mode = config.get('manual_refresh_mode', False) | ||||
|         self._serial = config.get('serial', '') | ||||
|         self._usage_hours = config.get('usage_hours', 0) | ||||
|         self._username = config.get('username', '') | ||||
|         self._enable_camera = config.get('enable_camera', True) | ||||
|  | ||||
|         self._connected = False | ||||
|         self._port = 1883 | ||||
|         self._refreshed = False | ||||
|  | ||||
|         self._device = Device(self) | ||||
|         self.bambu_cloud = BambuCloud( | ||||
|             config.get('region', ''), | ||||
|             config.get('email', ''), | ||||
|             config.get('username', ''), | ||||
|             config.get('auth_token', '') | ||||
|         ) | ||||
|         self.slicer_settings = SlicerSettings(self) | ||||
|  | ||||
|     @property | ||||
|     def connected(self): | ||||
|         """Return if connected to server""" | ||||
|         return self._connected | ||||
|  | ||||
|     @property | ||||
|     def manual_refresh_mode(self): | ||||
|         """Return if the integration is running in poll mode""" | ||||
|         return self._manual_refresh_mode | ||||
|  | ||||
|     async def set_manual_refresh_mode(self, on): | ||||
|         self._manual_refresh_mode = on | ||||
|         if self._manual_refresh_mode: | ||||
|             # Disconnect from the server. User must manually hit the refresh button to connect to refresh and then it will immediately disconnect. | ||||
|             self.disconnect() | ||||
|         else: | ||||
|             # Reconnect normally | ||||
|             self.connect(self._callback) | ||||
|  | ||||
|     @property | ||||
|     def camera_enabled(self): | ||||
|         return self._enable_camera | ||||
|      | ||||
|     def callback(self, event: str): | ||||
|         if self._callback is not None: | ||||
|             self._callback(event) | ||||
|  | ||||
|     def set_camera_enabled(self, enable): | ||||
|         self._enable_camera = enable | ||||
|         if self._enable_camera: | ||||
|             self._start_camera() | ||||
|         else: | ||||
|             self._stop_camera() | ||||
|  | ||||
|     def setup_tls(self): | ||||
|         self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE) | ||||
|         self.client.tls_insecure_set(True) | ||||
|  | ||||
|     def connect(self, callback): | ||||
|         """Connect to the MQTT Broker""" | ||||
|         self.client = mqtt.Client() | ||||
|         self._callback = callback | ||||
|         self.client.on_connect = self.on_connect | ||||
|         self.client.on_disconnect = self.on_disconnect | ||||
|         self.client.on_message = self.on_message | ||||
|         # Set aggressive reconnect polling. | ||||
|         self.client.reconnect_delay_set(min_delay=1, max_delay=1) | ||||
|  | ||||
|         # Run the blocking tls_set method in a separate thread | ||||
|         self.setup_tls() | ||||
|  | ||||
|         self._port = 8883 | ||||
|         if self._local_mqtt: | ||||
|             self.client.username_pw_set("bblp", password=self._access_code) | ||||
|         else: | ||||
|             self.client.username_pw_set(self._username, password=self._auth_token) | ||||
|  | ||||
|         LOGGER.debug("Starting MQTT listener thread") | ||||
|         self._mqtt = MqttThread(self) | ||||
|         self._mqtt.start() | ||||
|  | ||||
|     def subscribe_and_request_info(self): | ||||
|         LOGGER.debug("Loading slicer settings...") | ||||
|         self.slicer_settings.update() | ||||
|         LOGGER.debug("Now subscribing...") | ||||
|         self.subscribe() | ||||
|         LOGGER.debug("On Connect: Getting version info") | ||||
|         self.publish(GET_VERSION) | ||||
|         LOGGER.debug("On Connect: Request push all") | ||||
|         self.publish(PUSH_ALL) | ||||
|  | ||||
|     def on_connect(self, | ||||
|                    client_: mqtt.Client, | ||||
|                    userdata: None, | ||||
|                    flags: dict[str, Any], | ||||
|                    result_code: int, | ||||
|                    properties: mqtt.Properties | None = None, ): | ||||
|         """Handle connection""" | ||||
|         LOGGER.info("On Connect: Connected to printer") | ||||
|         self._on_connect() | ||||
|  | ||||
|     def _start_camera(self): | ||||
|         if not self._device.supports_feature(Features.CAMERA_RTSP): | ||||
|             if self._device.supports_feature(Features.CAMERA_IMAGE): | ||||
|                 if self._enable_camera: | ||||
|                     LOGGER.debug("Starting Chamber Image thread") | ||||
|                     self._camera = ChamberImageThread(self) | ||||
|                     self._camera.start() | ||||
|             elif (self.host == "") or (self._access_code == ""): | ||||
|                 LOGGER.debug("Skipping camera setup as local access details not provided.") | ||||
|  | ||||
|     def _stop_camera(self): | ||||
|         if self._camera is not None: | ||||
|             LOGGER.debug("Stopping camera thread") | ||||
|             self._camera.stop() | ||||
|             self._camera.join() | ||||
|  | ||||
|     def _on_connect(self): | ||||
|         self._connected = True | ||||
|         self.subscribe_and_request_info() | ||||
|  | ||||
|         LOGGER.debug("Starting watchdog thread") | ||||
|         self._watchdog = WatchdogThread(self) | ||||
|         self._watchdog.start() | ||||
|  | ||||
|         self._start_camera() | ||||
|  | ||||
|     def try_on_connect(self, | ||||
|                        client_: mqtt.Client, | ||||
|                        userdata: None, | ||||
|                        flags: dict[str, Any], | ||||
|                        result_code: int, | ||||
|                        properties: mqtt.Properties | None = None, ): | ||||
|         """Handle connection""" | ||||
|         LOGGER.info("On Connect: Connected to printer") | ||||
|         self._connected = True | ||||
|         LOGGER.debug("Now test subscribing...") | ||||
|         self.subscribe() | ||||
|         # For the initial configuration connection attempt, we just need version info. | ||||
|         LOGGER.debug("On Connect: Getting version info") | ||||
|         self.publish(GET_VERSION) | ||||
|  | ||||
|     def on_disconnect(self, | ||||
|                       client_: mqtt.Client, | ||||
|                       userdata: None, | ||||
|                       result_code: int): | ||||
|         """Called when MQTT Disconnects""" | ||||
|         LOGGER.warn(f"On Disconnect: Printer disconnected with error code: {result_code}") | ||||
|         self._on_disconnect() | ||||
|      | ||||
|     def _on_disconnect(self): | ||||
|         LOGGER.debug("_on_disconnect: Lost connection to the printer") | ||||
|         self._connected = False | ||||
|         self._device.info.set_online(False) | ||||
|         if self._watchdog is not None: | ||||
|             LOGGER.debug("Stopping watchdog thread") | ||||
|             self._watchdog.stop() | ||||
|             self._watchdog.join() | ||||
|         self._stop_camera() | ||||
|  | ||||
|     def _on_watchdog_fired(self): | ||||
|         LOGGER.info("Watch dog fired") | ||||
|         self._device.info.set_online(False) | ||||
|         self.publish(START_PUSH) | ||||
|  | ||||
|     def on_jpeg_received(self, bytes): | ||||
|         self._device.chamber_image.set_jpeg(bytes) | ||||
|  | ||||
|     def on_message(self, client, userdata, message): | ||||
|         """Return the payload when received""" | ||||
|         try: | ||||
|             # X1 mqtt payload is inconsistent. Adjust it for consistent logging. | ||||
|             clean_msg = re.sub(r"\\n *", "", str(message.payload)) | ||||
|             if self._refreshed: | ||||
|                 LOGGER.debug(f"Received data: {clean_msg}") | ||||
|  | ||||
|             json_data = json.loads(message.payload) | ||||
|             if json_data.get("event"): | ||||
|                 # These are events from the bambu cloud mqtt feed and allow us to detect when a local | ||||
|                 # device has connected/disconnected (e.g. turned on/off) | ||||
|                 if json_data.get("event").get("event") == "client.connected": | ||||
|                     LOGGER.debug("Client connected event received.") | ||||
|                     self._on_disconnect() # We aren't guaranteed to recieve a client.disconnected event. | ||||
|                     self._on_connect() | ||||
|                 elif json_data.get("event").get("event") == "client.disconnected": | ||||
|                     LOGGER.debug("Client disconnected event received.") | ||||
|                     self._on_disconnect() | ||||
|             else: | ||||
|                 self._device.info.set_online(True) | ||||
|                 self._watchdog.received_data() | ||||
|                 if json_data.get("print"): | ||||
|                     self._device.print_update(data=json_data.get("print")) | ||||
|                     # Once we receive data, if in manual refresh mode, we disconnect again. | ||||
|                     if self._manual_refresh_mode: | ||||
|                         self.disconnect() | ||||
|                     if json_data.get("print").get("msg", 0) == 0: | ||||
|                         self._refreshed= False | ||||
|                 elif json_data.get("info") and json_data.get("info").get("command") == "get_version": | ||||
|                     LOGGER.debug("Got Version Data") | ||||
|                     self._device.info_update(data=json_data.get("info")) | ||||
|         except Exception as e: | ||||
|             LOGGER.error("An exception occurred processing a message:", exc_info=e) | ||||
|  | ||||
|     def subscribe(self): | ||||
|         """Subscribe to report topic""" | ||||
|         LOGGER.debug(f"Subscribing: device/{self._serial}/report") | ||||
|         self.client.subscribe(f"device/{self._serial}/report") | ||||
|  | ||||
|     def publish(self, msg): | ||||
|         """Publish a custom message""" | ||||
|         result = self.client.publish(f"device/{self._serial}/request", json.dumps(msg)) | ||||
|         status = result[0] | ||||
|         if status == 0: | ||||
|             LOGGER.debug(f"Sent {msg} to topic device/{self._serial}/request") | ||||
|             return True | ||||
|  | ||||
|         LOGGER.error(f"Failed to send message to topic device/{self._serial}/request") | ||||
|         return False | ||||
|  | ||||
|     async def refresh(self): | ||||
|         """Force refresh data""" | ||||
|  | ||||
|         if self._manual_refresh_mode: | ||||
|             self.connect(self._callback) | ||||
|         else: | ||||
|             LOGGER.debug("Force Refresh: Getting Version Info") | ||||
|             self._refreshed = True | ||||
|             self.publish(GET_VERSION) | ||||
|             LOGGER.debug("Force Refresh: Request Push All") | ||||
|             self._refreshed = True | ||||
|             self.publish(PUSH_ALL) | ||||
|  | ||||
|         self.slicer_settings.update() | ||||
|  | ||||
|     def get_device(self): | ||||
|         """Return device""" | ||||
|         return self._device | ||||
|  | ||||
|     def disconnect(self): | ||||
|         """Disconnect the Bambu Client from server""" | ||||
|         LOGGER.debug(" Disconnect: Client Disconnecting") | ||||
|         if self.client is not None: | ||||
|             self.client.disconnect() | ||||
|             self.client = None | ||||
|  | ||||
|     async def try_connection(self): | ||||
|         """Test if we can connect to an MQTT broker.""" | ||||
|         LOGGER.debug("Try Connection") | ||||
|  | ||||
|         result: queue.Queue[bool] = queue.Queue(maxsize=1) | ||||
|  | ||||
|         def on_message(client, userdata, message): | ||||
|             json_data = json.loads(message.payload) | ||||
|             LOGGER.debug(f"Try Connection: Got '{json_data}'") | ||||
|             if json_data.get("info") and json_data.get("info").get("command") == "get_version": | ||||
|                 LOGGER.debug("Got Version Command Data") | ||||
|                 self._device.info_update(data=json_data.get("info")) | ||||
|                 result.put(True) | ||||
|  | ||||
|         self.client = mqtt.Client() | ||||
|         self.client.on_connect = self.try_on_connect | ||||
|         self.client.on_disconnect = self.on_disconnect | ||||
|         self.client.on_message = on_message | ||||
|  | ||||
|         # Run the blocking tls_set method in a separate thread | ||||
|         loop = asyncio.get_event_loop() | ||||
|         await loop.run_in_executor(None, self.setup_tls) | ||||
|          | ||||
|         if self._local_mqtt: | ||||
|             self.client.username_pw_set("bblp", password=self._access_code) | ||||
|         else: | ||||
|             self.client.username_pw_set(self._username, password=self._auth_token) | ||||
|         self._port = 8883 | ||||
|  | ||||
|         LOGGER.debug("Test connection: Connecting to %s", self.host) | ||||
|         try: | ||||
|             self.client.connect(self.host, self._port) | ||||
|             self.client.loop_start() | ||||
|             if result.get(timeout=10): | ||||
|                 return True | ||||
|         except OSError as e: | ||||
|             return False | ||||
|         except queue.Empty: | ||||
|             return False | ||||
|         finally: | ||||
|             self.disconnect() | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         """Async enter. | ||||
|         Returns: | ||||
|             The BambuLab object. | ||||
|         """ | ||||
|         return self | ||||
|  | ||||
|     async def __aexit__(self, *_exc_info): | ||||
|         """Async exit. | ||||
|         Args: | ||||
|             _exc_info: Exec type. | ||||
|         """ | ||||
|         self.disconnect() | ||||
| @@ -1,583 +0,0 @@ | ||||
| from __future__ import annotations | ||||
| from enum import ( | ||||
|     Enum, | ||||
| ) | ||||
|  | ||||
| import base64 | ||||
| import cloudscraper | ||||
| import json | ||||
| import requests | ||||
|  | ||||
| class ConnectionMechanismEnum(Enum): | ||||
|     CLOUDSCRAPER = 1, | ||||
|     CURL_CFFI = 2, | ||||
|     REQUESTS = 3 | ||||
|  | ||||
| CONNECTION_MECHANISM = ConnectionMechanismEnum.CLOUDSCRAPER | ||||
|  | ||||
| curl_available = False | ||||
| if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI: | ||||
|     try: | ||||
|         from curl_cffi import requests as curl_requests | ||||
|         curl_available = True | ||||
|     except ImportError: | ||||
|         curl_available = False | ||||
|  | ||||
| from dataclasses import dataclass | ||||
|  | ||||
| from .const import ( | ||||
|      LOGGER, | ||||
|      BambuUrl | ||||
| ) | ||||
|  | ||||
| from .utils import get_Url | ||||
|  | ||||
| IMPERSONATE_BROWSER='chrome' | ||||
|  | ||||
| class CloudflareError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("Blocked by Cloudflare") | ||||
|         self.error_code = 403 | ||||
|  | ||||
| class EmailCodeRequiredError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("Email code required") | ||||
|         self.error_code = 400 | ||||
|  | ||||
| class EmailCodeExpiredError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("Email code expired") | ||||
|         self.error_code = 400 | ||||
|  | ||||
| class EmailCodeIncorrectError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("Email code incorrect") | ||||
|         self.error_code = 400 | ||||
|  | ||||
| class TfaCodeRequiredError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("Two factor authentication code required") | ||||
|         self.error_code = 400 | ||||
|  | ||||
| class CurlUnavailableError(Exception): | ||||
|     def __init__(self): | ||||
|         super().__init__("curl library unavailable") | ||||
|         self.error_code = 400 | ||||
|  | ||||
| @dataclass | ||||
| class BambuCloud: | ||||
|    | ||||
|     def __init__(self, region: str, email: str, username: str, auth_token: str): | ||||
|         self._region = region | ||||
|         self._email = email | ||||
|         self._username = username | ||||
|         self._auth_token = auth_token | ||||
|         self._tfaKey = None | ||||
|  | ||||
|     def _get_headers(self): | ||||
|         return { | ||||
|             'User-Agent': 'bambu_network_agent/01.09.05.01', | ||||
|             'X-BBL-Client-Name': 'OrcaSlicer', | ||||
|             'X-BBL-Client-Type': 'slicer', | ||||
|             'X-BBL-Client-Version': '01.09.05.51', | ||||
|             'X-BBL-Language': 'en-US', | ||||
|             'X-BBL-OS-Type': 'linux', | ||||
|             'X-BBL-OS-Version': '6.2.0', | ||||
|             'X-BBL-Agent-Version': '01.09.05.01', | ||||
|             'X-BBL-Executable-info': '{}', | ||||
|             'X-BBL-Agent-OS-Type': 'linux', | ||||
|             'accept': 'application/json', | ||||
|             'Content-Type': 'application/json' | ||||
|         } | ||||
|         # Orca/Bambu Studio also add this - need to work out what an appropriate ID is to put here: | ||||
|         # 'X-BBL-Device-ID': BBL_AUTH_UUID, | ||||
|         # Example: X-BBL-Device-ID: 370f9f43-c6fe-47d7-aec9-5fe5ef7e7673 | ||||
|  | ||||
|     def _get_headers_with_auth_token(self) -> dict: | ||||
|         if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI: | ||||
|             headers = {} | ||||
|         else: | ||||
|             headers = self._get_headers() | ||||
|         headers['Authorization'] = f"Bearer {self._auth_token}" | ||||
|         return headers | ||||
|  | ||||
|     def _test_response(self, response, return400=False): | ||||
|         # Check specifically for cloudflare block | ||||
|         if response.status_code == 403 and 'cloudflare' in response.text: | ||||
|             LOGGER.debug("BLOCKED BY CLOUDFLARE") | ||||
|             raise CloudflareError() | ||||
|  | ||||
|         if response.status_code == 400 and not return400: | ||||
|             LOGGER.error(f"Connection failed with error code: {response.status_code}") | ||||
|             LOGGER.debug(f"Response: '{response.text}'") | ||||
|             raise PermissionError(response.status_code, response.text) | ||||
|  | ||||
|         if response.status_code > 400: | ||||
|             LOGGER.error(f"Connection failed with error code: {response.status_code}") | ||||
|             LOGGER.debug(f"Response: '{response.text}'") | ||||
|             raise PermissionError(response.status_code, response.text) | ||||
|  | ||||
|         LOGGER.debug(f"Response: {response.status_code}") | ||||
|  | ||||
|     def _get(self, urlenum: BambuUrl): | ||||
|         url = get_Url(urlenum, self._region) | ||||
|         headers=self._get_headers_with_auth_token() | ||||
|         if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI: | ||||
|             if not curl_available: | ||||
|                 LOGGER.debug(f"Curl library is unavailable.") | ||||
|                 raise CurlUnavailableError() | ||||
|             response = curl_requests.get(url, headers=headers, timeout=10, impersonate=IMPERSONATE_BROWSER) | ||||
|         elif CONNECTION_MECHANISM == ConnectionMechanismEnum.CLOUDSCRAPER: | ||||
|             if len(headers) == 0: | ||||
|                 headers = self._get_headers() | ||||
|             scraper = cloudscraper.create_scraper() | ||||
|             response = scraper.get(url, headers=headers, timeout=10) | ||||
|         elif CONNECTION_MECHANISM == ConnectionMechanismEnum.REQUESTS: | ||||
|             if len(headers) == 0: | ||||
|                 headers = self._get_headers() | ||||
|             response = requests.get(url, headers=headers, timeout=10) | ||||
|         else: | ||||
|             raise NotImplementedError() | ||||
|  | ||||
|         self._test_response(response) | ||||
|  | ||||
|         return response | ||||
|  | ||||
|  | ||||
|     def _post(self, urlenum: BambuUrl, json: str, headers={}, return400=False): | ||||
|         url = get_Url(urlenum, self._region) | ||||
|         if CONNECTION_MECHANISM == ConnectionMechanismEnum.CURL_CFFI: | ||||
|             if not curl_available: | ||||
|                 LOGGER.debug(f"Curl library is unavailable.") | ||||
|                 raise CurlUnavailableError() | ||||
|             response = curl_requests.post(url, headers=headers, json=json, impersonate=IMPERSONATE_BROWSER) | ||||
|         elif CONNECTION_MECHANISM == ConnectionMechanismEnum.CLOUDSCRAPER: | ||||
|             if len(headers) == 0: | ||||
|                 headers = self._get_headers() | ||||
|             scraper = cloudscraper.create_scraper() | ||||
|             response = scraper.post(url, headers=headers, json=json) | ||||
|         elif CONNECTION_MECHANISM == ConnectionMechanismEnum.REQUESTS: | ||||
|             if len(headers) == 0: | ||||
|                 headers = self._get_headers() | ||||
|             response = requests.post(url, headers=headers, json=json) | ||||
|         else: | ||||
|             raise NotImplementedError() | ||||
|  | ||||
|         self._test_response(response, return400) | ||||
|          | ||||
|         return response | ||||
|  | ||||
|     def _get_authentication_token(self) -> str: | ||||
|         LOGGER.debug("Getting accessToken from Bambu Cloud") | ||||
|  | ||||
|         # First we need to find out how Bambu wants us to login. | ||||
|         data = { | ||||
|             "account": self._email, | ||||
|             "password": self._password, | ||||
|             "apiError": "" | ||||
|         } | ||||
|  | ||||
|         response = self._post(BambuUrl.LOGIN, json=data) | ||||
|  | ||||
|         auth_json = response.json() | ||||
|         accessToken = auth_json.get('accessToken', '') | ||||
|         if accessToken != '': | ||||
|             # We were provided the accessToken directly. | ||||
|             return accessToken | ||||
|          | ||||
|         loginType = auth_json.get("loginType", None) | ||||
|         if loginType is None: | ||||
|             LOGGER.error(f"loginType not present") | ||||
|             LOGGER.error(f"Response not understood: '{response.text}'") | ||||
|             return ValueError(0) # FIXME | ||||
|         elif loginType == 'verifyCode': | ||||
|             LOGGER.debug(f"Received verifyCode response") | ||||
|             raise EmailCodeRequiredError() | ||||
|         elif loginType == 'tfa': | ||||
|             # Store the tfaKey for later use | ||||
|             LOGGER.debug(f"Received tfa response") | ||||
|             self._tfaKey = auth_json.get("tfaKey") | ||||
|             raise TfaCodeRequiredError() | ||||
|         else: | ||||
|             LOGGER.debug(f"Did not understand json. loginType = '{loginType}'") | ||||
|             LOGGER.error(f"Response not understood: '{response.text}'") | ||||
|             return ValueError(1) # FIXME | ||||
|      | ||||
|     def _get_email_verification_code(self): | ||||
|         # Send the verification code request | ||||
|         data = { | ||||
|             "email": self._email, | ||||
|             "type": "codeLogin" | ||||
|         } | ||||
|  | ||||
|         LOGGER.debug("Requesting verification code") | ||||
|         self._post(BambuUrl.EMAIL_CODE, json=data) | ||||
|         LOGGER.debug("Verification code requested successfully.") | ||||
|  | ||||
|     def _get_authentication_token_with_verification_code(self, code) -> dict: | ||||
|         LOGGER.debug("Attempting to connect with provided verification code.") | ||||
|         data = { | ||||
|             "account": self._email, | ||||
|             "code": code | ||||
|         } | ||||
|  | ||||
|         response = self._post(BambuUrl.LOGIN, json=data, return400=True) | ||||
|         status_code = response.status_code | ||||
|  | ||||
|         if status_code == 200: | ||||
|             LOGGER.debug("Authentication successful.") | ||||
|             LOGGER.debug(f"Response = '{response.json()}'") | ||||
|         elif status_code == 400: | ||||
|             LOGGER.debug(f"Received response: {response.json()}")            | ||||
|             if response.json()['code'] == 1: | ||||
|                 # Code has expired. Request a new one. | ||||
|                 self._get_email_verification_code() | ||||
|                 raise EmailCodeExpiredError() | ||||
|             elif response.json()['code'] == 2: | ||||
|                 # Code was incorrect. Let the user try again. | ||||
|                 raise EmailCodeIncorrectError() | ||||
|             else: | ||||
|                 LOGGER.error(f"Response not understood: '{response.json()}'") | ||||
|                 raise ValueError(response.json()['code']) | ||||
|  | ||||
|         return response.json()['accessToken'] | ||||
|      | ||||
|     def _get_authentication_token_with_2fa_code(self, code: str) -> dict: | ||||
|         LOGGER.debug("Attempting to connect with provided 2FA code.") | ||||
|  | ||||
|         data = { | ||||
|             "tfaKey": self._tfaKey, | ||||
|             "tfaCode": code | ||||
|         } | ||||
|  | ||||
|         response = self._post(BambuUrl.TFA_LOGIN, json=data) | ||||
|  | ||||
|         LOGGER.debug(f"Response: {response.status_code}") | ||||
|         if response.status_code == 200: | ||||
|             LOGGER.debug("Authentication successful.") | ||||
|  | ||||
|         cookies = response.cookies.get_dict() | ||||
|         token_from_tfa = cookies.get("token") | ||||
|         #LOGGER.debug(f"token_from_tfa: {token_from_tfa}") | ||||
|  | ||||
|         return token_from_tfa | ||||
|      | ||||
|     def _get_username_from_authentication_token(self) -> str: | ||||
|         LOGGER.debug("Trying to get username from authentication token.") | ||||
|         # User name is in 2nd portion of the auth token (delimited with periods) | ||||
|         username = None | ||||
|         tokens = self._auth_token.split(".") | ||||
|         if len(tokens) != 3: | ||||
|             LOGGER.debug("Received authToken is not a JWT.") | ||||
|             LOGGER.debug("Trying to use project API to retrieve username instead") | ||||
|             response = self.get_projects(); | ||||
|             if response is not None: | ||||
|                 projectsnode = response.get('projects', None) | ||||
|                 if projectsnode is None: | ||||
|                     LOGGER.debug("Failed to find projects node") | ||||
|                 else: | ||||
|                     if len(projectsnode) == 0: | ||||
|                         LOGGER.debug("No projects node in response") | ||||
|                     else: | ||||
|                         project=projectsnode[0] | ||||
|                         if project.get('user_id', None) is None: | ||||
|                             LOGGER.debug("No user_id entry") | ||||
|                         else: | ||||
|                             username = f"u_{project['user_id']}" | ||||
|                             LOGGER.debug(f"Found user_id of {username}") | ||||
|         else: | ||||
|             LOGGER.debug("Authentication token looks to be a JWT") | ||||
|             try: | ||||
|                 b64_string = self._auth_token.split(".")[1] | ||||
|                 # String must be multiples of 4 chars in length. For decode pad with = character | ||||
|                 b64_string += "=" * ((4 - len(b64_string) % 4) % 4) | ||||
|                 jsonAuthToken = json.loads(base64.b64decode(b64_string)) | ||||
|                 # Gives json payload with "username":"u_<digits>" within it | ||||
|                 username = jsonAuthToken.get('username', None) | ||||
|             except: | ||||
|                 LOGGER.debug("Unable to decode authToken to json to retrieve username.") | ||||
|  | ||||
|         if username is None: | ||||
|             LOGGER.debug(f"Unable to decode authToken to retrieve username. AuthToken = {self._auth_token}") | ||||
|  | ||||
|         return username | ||||
|      | ||||
|     # Retrieves json description of devices in the form: | ||||
|     # { | ||||
|     #     'message': 'success', | ||||
|     #     'code': None, | ||||
|     #     'error': None, | ||||
|     #     'devices': [ | ||||
|     #         { | ||||
|     #             'dev_id': 'REDACTED', | ||||
|     #             'name': 'Bambu P1S', | ||||
|     #             'online': True, | ||||
|     #             'print_status': 'SUCCESS', | ||||
|     #             'dev_model_name': 'C12', | ||||
|     #             'dev_product_name': 'P1S', | ||||
|     #             'dev_access_code': 'REDACTED', | ||||
|     #             'nozzle_diameter': 0.4 | ||||
|     #         }, | ||||
|     #         { | ||||
|     #             'dev_id': 'REDACTED', | ||||
|     #             'name': 'Bambu P1P', | ||||
|     #             'online': True, | ||||
|     #             'print_status': 'RUNNING', | ||||
|     #             'dev_model_name': 'C11', | ||||
|     #             'dev_product_name': 'P1P', | ||||
|     #             'dev_access_code': 'REDACTED', | ||||
|     #             'nozzle_diameter': 0.4 | ||||
|     #         }, | ||||
|     #         { | ||||
|     #             'dev_id': 'REDACTED', | ||||
|     #             'name': 'Bambu X1C', | ||||
|     #             'online': True, | ||||
|     #             'print_status': 'RUNNING', | ||||
|     #             'dev_model_name': 'BL-P001', | ||||
|     #             'dev_product_name': 'X1 Carbon', | ||||
|     #             'dev_access_code': 'REDACTED', | ||||
|     #             'nozzle_diameter': 0.4 | ||||
|     #         } | ||||
|     #     ] | ||||
|     # } | ||||
|      | ||||
|     def test_authentication(self, region: str, email: str, username: str, auth_token: str) -> bool: | ||||
|         self._region = region | ||||
|         self._email = email | ||||
|         self._username = username | ||||
|         self._auth_token = auth_token | ||||
|         try: | ||||
|             self.get_device_list() | ||||
|         except: | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|     def login(self, region: str, email: str, password: str) -> str: | ||||
|         self._region = region | ||||
|         self._email = email | ||||
|         self._password = password | ||||
|  | ||||
|         result = self._get_authentication_token() | ||||
|         self._auth_token = result | ||||
|         self._username = self._get_username_from_authentication_token() | ||||
|          | ||||
|     def login_with_verification_code(self, code: str): | ||||
|         result = self._get_authentication_token_with_verification_code(code) | ||||
|         self._auth_token = result | ||||
|         self._username = self._get_username_from_authentication_token() | ||||
|  | ||||
|     def login_with_2fa_code(self, code: str): | ||||
|         result = self._get_authentication_token_with_2fa_code(code) | ||||
|         self._auth_token = result | ||||
|         self._username = self._get_username_from_authentication_token() | ||||
|  | ||||
|     def get_device_list(self) -> dict: | ||||
|         LOGGER.debug("Getting device list from Bambu Cloud") | ||||
|         try: | ||||
|             response = self._get(BambuUrl.BIND) | ||||
|         except: | ||||
|             return None | ||||
|         return response.json()['devices'] | ||||
|  | ||||
|     # The slicer settings are of the following form: | ||||
|     # | ||||
|     # { | ||||
|     #     "message": "success", | ||||
|     #     "code": null, | ||||
|     #     "error": null, | ||||
|     #     "print": { | ||||
|     #         "public": [ | ||||
|     #             { | ||||
|     #                 "setting_id": "GP004", | ||||
|     #                 "version": "01.09.00.15", | ||||
|     #                 "name": "0.20mm Standard @BBL X1C", | ||||
|     #                 "update_time": "2024-07-04 11:27:08", | ||||
|     #                 "nickname": null | ||||
|     #             }, | ||||
|     #             ... | ||||
|     #         } | ||||
|     #         "private": [] | ||||
|     #     }, | ||||
|     #     "printer": { | ||||
|     #         "public": [ | ||||
|     #             { | ||||
|     #                 "setting_id": "GM001", | ||||
|     #                 "version": "01.09.00.15", | ||||
|     #                 "name": "Bambu Lab X1 Carbon 0.4 nozzle", | ||||
|     #                 "update_time": "2024-07-04 11:25:07", | ||||
|     #                 "nickname": null | ||||
|     #             }, | ||||
|     #             ... | ||||
|     #         ], | ||||
|     #         "private": [] | ||||
|     #     }, | ||||
|     #     "filament": { | ||||
|     #         "public": [ | ||||
|     #             { | ||||
|     #                 "setting_id": "GFSA01", | ||||
|     #                 "version": "01.09.00.15", | ||||
|     #                 "name": "Bambu PLA Matte @BBL X1C", | ||||
|     #                 "update_time": "2024-07-04 11:29:21", | ||||
|     #                 "nickname": null, | ||||
|     #                 "filament_id": "GFA01" | ||||
|     #             }, | ||||
|     #             ... | ||||
|     #         ], | ||||
|     #         "private": [ | ||||
|     #             { | ||||
|     #                 "setting_id": "PFUS46ea5c221cabe5", | ||||
|     #                 "version": "1.9.0.14", | ||||
|     #                 "name": "Fillamentum PLA Extrafill @Bambu Lab X1 Carbon 0.4 nozzle", | ||||
|     #                 "update_time": "2024-07-10 06:48:17", | ||||
|     #                 "base_id": null, | ||||
|     #                 "filament_id": "Pc628b24", | ||||
|     #                 "filament_type": "PLA", | ||||
|     #                 "filament_is_support": "0", | ||||
|     #                 "nozzle_temperature": [ | ||||
|     #                     190, | ||||
|     #                     240 | ||||
|     #                 ], | ||||
|     #                 "nozzle_hrc": "3", | ||||
|     #                 "filament_vendor": "Fillamentum" | ||||
|     #             }, | ||||
|     #             ... | ||||
|     #         ] | ||||
|     #     }, | ||||
|     #     "settings": {} | ||||
|     # } | ||||
|  | ||||
|     def get_slicer_settings(self) -> dict: | ||||
|         LOGGER.debug("Getting slicer settings from Bambu Cloud") | ||||
|         try: | ||||
|             response = self._get(BambuUrl.SLICER_SETTINGS) | ||||
|         except: | ||||
|             return None | ||||
|         LOGGER.debug("Succeeded") | ||||
|         return response.json() | ||||
|          | ||||
|     # The task list is of the following form with a 'hits' array with typical 20 entries. | ||||
|     # | ||||
|     # "total": 531, | ||||
|     # "hits": [ | ||||
|     #     { | ||||
|     #     "id": 35237965, | ||||
|     #     "designId": 0, | ||||
|     #     "designTitle": "", | ||||
|     #     "instanceId": 0, | ||||
|     #     "modelId": "REDACTED", | ||||
|     #     "title": "REDACTED", | ||||
|     #     "cover": "REDACTED", | ||||
|     #     "status": 4, | ||||
|     #     "feedbackStatus": 0, | ||||
|     #     "startTime": "2023-12-21T19:02:16Z", | ||||
|     #     "endTime": "2023-12-21T19:02:35Z", | ||||
|     #     "weight": 34.62, | ||||
|     #     "length": 1161, | ||||
|     #     "costTime": 10346, | ||||
|     #     "profileId": 35276233, | ||||
|     #     "plateIndex": 1, | ||||
|     #     "plateName": "", | ||||
|     #     "deviceId": "REDACTED", | ||||
|     #     "amsDetailMapping": [ | ||||
|     #         { | ||||
|     #         "ams": 4, | ||||
|     #         "sourceColor": "F4D976FF", | ||||
|     #         "targetColor": "F4D976FF", | ||||
|     #         "filamentId": "GFL99", | ||||
|     #         "filamentType": "PLA", | ||||
|     #         "targetFilamentType": "", | ||||
|     #         "weight": 34.62 | ||||
|     #         } | ||||
|     #     ], | ||||
|     #     "mode": "cloud_file", | ||||
|     #     "isPublicProfile": false, | ||||
|     #     "isPrintable": true, | ||||
|     #     "deviceModel": "P1P", | ||||
|     #     "deviceName": "Bambu P1P", | ||||
|     #     "bedType": "textured_plate" | ||||
|     #     }, | ||||
|  | ||||
|     def get_tasklist(self) -> dict: | ||||
|         LOGGER.debug("Getting full task list from Bambu Cloud") | ||||
|         try: | ||||
|             response = self._get(BambuUrl.TASKS) | ||||
|         except: | ||||
|             return None | ||||
|         return response.json() | ||||
|  | ||||
|     # Returns a list of projects for the account. | ||||
|     # | ||||
|     # { | ||||
|     # "message": "success", | ||||
|     # "code": null, | ||||
|     # "error": null, | ||||
|     # "projects": [ | ||||
|     #     { | ||||
|     #     "project_id": "164995388", | ||||
|     #     "user_id": "1688388450", | ||||
|     #     "model_id": "US48e2103d939bf8", | ||||
|     #     "status": "ACTIVE", | ||||
|     #     "name": "Alcohol_Marker_Storage_for_Copic,_Ohuhu_and_the_like", | ||||
|     #     "content": "{'printed_plates': [{'plate': 1}]}", | ||||
|     #     "create_time": "2024-11-17 06:12:33", | ||||
|     #     "update_time": "2024-11-17 06:12:40" | ||||
|     #     }, | ||||
|     #     ... | ||||
|     # | ||||
|     def get_projects(self) -> dict: | ||||
|         LOGGER.debug("Getting projects list from Bambu Cloud") | ||||
|         try: | ||||
|             response = self._get(BambuUrl.PROJECTS) | ||||
|         except: | ||||
|             return None | ||||
|         return response.json() | ||||
|  | ||||
|     def get_latest_task_for_printer(self, deviceId: str) -> dict: | ||||
|         LOGGER.debug(f"Getting latest task for printer from Bambu Cloud") | ||||
|         try: | ||||
|             data = self.get_tasklist_for_printer(deviceId) | ||||
|             if len(data) != 0: | ||||
|                 return data[0] | ||||
|             LOGGER.debug("No tasks found for printer") | ||||
|             return None | ||||
|         except: | ||||
|             return None | ||||
|  | ||||
|     def get_tasklist_for_printer(self, deviceId: str) -> dict: | ||||
|         LOGGER.debug(f"Getting full task list for printer from Bambu Cloud") | ||||
|         tasks = [] | ||||
|         data = self.get_tasklist() | ||||
|         for task in data['hits']: | ||||
|             if task['deviceId'] == deviceId: | ||||
|                 tasks.append(task) | ||||
|         return tasks | ||||
|  | ||||
|     def get_device_type_from_device_product_name(self, device_product_name: str): | ||||
|         if device_product_name == "X1 Carbon": | ||||
|             return "X1C" | ||||
|         return device_product_name.replace(" ", "") | ||||
|  | ||||
|     def download(self, url: str) -> bytearray: | ||||
|         LOGGER.debug(f"Downloading cover image: {url}") | ||||
|         try: | ||||
|             # This is just a standard download from an unauthenticated end point. | ||||
|             response = requests.get(url) | ||||
|         except: | ||||
|             return None | ||||
|         return response.content | ||||
|  | ||||
|     @property | ||||
|     def username(self): | ||||
|         return self._username | ||||
|      | ||||
|     @property | ||||
|     def auth_token(self): | ||||
|         return self._auth_token | ||||
|      | ||||
|     @property | ||||
|     def bambu_connected(self) -> bool: | ||||
|         return self._auth_token != "" and self._auth_token != None | ||||
|      | ||||
|     @property | ||||
|     def cloud_mqtt_host(self): | ||||
|         return "cn.mqtt.bambulab.com" if self._region == "China" else "us.mqtt.bambulab.com" | ||||
| @@ -1,24 +0,0 @@ | ||||
| """MQTT Commands""" | ||||
| CHAMBER_LIGHT_ON = { | ||||
|     "system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "on", | ||||
|                "led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}} | ||||
| CHAMBER_LIGHT_OFF = { | ||||
|     "system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "off", | ||||
|                "led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}} | ||||
|  | ||||
| SPEED_PROFILE_TEMPLATE = {"print": {"sequence_id": "0", "command": "print_speed", "param": ""}} | ||||
|  | ||||
| GET_VERSION = {"info": {"sequence_id": "0", "command": "get_version"}} | ||||
|  | ||||
| PAUSE = {"print": {"sequence_id": "0", "command": "pause"}} | ||||
| RESUME = {"print": {"sequence_id": "0", "command": "resume"}} | ||||
| STOP = {"print": {"sequence_id": "0", "command": "stop"}} | ||||
|  | ||||
| PUSH_ALL = {"pushing": {"sequence_id": "0", "command": "pushall"}} | ||||
|  | ||||
| START_PUSH = { "pushing": {"sequence_id": "0", "command": "start"}} | ||||
|  | ||||
| SEND_GCODE_TEMPLATE = {"print": {"sequence_id": "0", "command": "gcode_line", "param": ""}} # param = GCODE_EACH_LINE_SEPARATED_BY_\n | ||||
|  | ||||
| # X1 only currently | ||||
| GET_ACCESSORIES = {"system": {"sequence_id": "0", "command": "get_accessories", "accessory_type": "none"}} | ||||
| @@ -1,199 +0,0 @@ | ||||
| import json | ||||
| import logging | ||||
|  | ||||
| from pathlib import Path | ||||
| from enum import ( | ||||
|     Enum, | ||||
|     IntEnum, | ||||
| ) | ||||
|  | ||||
| from .const_hms_errors import HMS_ERRORS | ||||
| # These errors cover those that are AMS and/or slot specific. | ||||
| # 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error. | ||||
| from .const_ams_errors import HMS_AMS_ERRORS | ||||
| from .const_print_errors import PRINT_ERROR_ERRORS | ||||
|  | ||||
| LOGGER = logging.getLogger(__package__) | ||||
|  | ||||
|  | ||||
| class Features(Enum): | ||||
|     AUX_FAN = 1, | ||||
|     CHAMBER_LIGHT = 2, | ||||
|     CHAMBER_FAN = 3, | ||||
|     CHAMBER_TEMPERATURE = 4, | ||||
|     CURRENT_STAGE = 5, | ||||
|     PRINT_LAYERS = 6, | ||||
|     AMS = 7, | ||||
|     EXTERNAL_SPOOL = 8, | ||||
|     K_VALUE = 9, | ||||
|     START_TIME = 10, | ||||
|     AMS_TEMPERATURE = 11, | ||||
|     CAMERA_RTSP = 13, | ||||
|     START_TIME_GENERATED = 14, | ||||
|     CAMERA_IMAGE = 15, | ||||
|     DOOR_SENSOR = 16, | ||||
|     MANUAL_MODE = 17, | ||||
|     AMS_FILAMENT_REMAINING = 18, | ||||
|     SET_TEMPERATURE = 19, | ||||
|  | ||||
|  | ||||
| class FansEnum(Enum): | ||||
|     PART_COOLING = 1, | ||||
|     AUXILIARY = 2, | ||||
|     CHAMBER = 3, | ||||
|     HEATBREAK = 4, | ||||
|  | ||||
|  | ||||
| class TempEnum(Enum): | ||||
|     HEATBED = 1, | ||||
|     NOZZLE = 2 | ||||
|  | ||||
|  | ||||
| CURRENT_STAGE_IDS = { | ||||
|     "default": "unknown", | ||||
|     0: "printing", | ||||
|     1: "auto_bed_leveling", | ||||
|     2: "heatbed_preheating", | ||||
|     3: "sweeping_xy_mech_mode", | ||||
|     4: "changing_filament", | ||||
|     5: "m400_pause", | ||||
|     6: "paused_filament_runout", | ||||
|     7: "heating_hotend", | ||||
|     8: "calibrating_extrusion", | ||||
|     9: "scanning_bed_surface", | ||||
|     10: "inspecting_first_layer", | ||||
|     11: "identifying_build_plate_type", | ||||
|     12: "calibrating_micro_lidar", # DUPLICATED? | ||||
|     13: "homing_toolhead", | ||||
|     14: "cleaning_nozzle_tip", | ||||
|     15: "checking_extruder_temperature", | ||||
|     16: "paused_user", | ||||
|     17: "paused_front_cover_falling", | ||||
|     18: "calibrating_micro_lidar", # DUPLICATED? | ||||
|     19: "calibrating_extrusion_flow", | ||||
|     20: "paused_nozzle_temperature_malfunction", | ||||
|     21: "paused_heat_bed_temperature_malfunction", | ||||
|     22: "filament_unloading", | ||||
|     23: "paused_skipped_step", | ||||
|     24: "filament_loading", | ||||
|     25: "calibrating_motor_noise", | ||||
|     26: "paused_ams_lost", | ||||
|     27: "paused_low_fan_speed_heat_break", | ||||
|     28: "paused_chamber_temperature_control_error", | ||||
|     29: "cooling_chamber", | ||||
|     30: "paused_user_gcode", | ||||
|     31: "motor_noise_showoff", | ||||
|     32: "paused_nozzle_filament_covered_detected", | ||||
|     33: "paused_cutter_error", | ||||
|     34: "paused_first_layer_error", | ||||
|     35: "paused_nozzle_clog", | ||||
|     # X1 returns -1 for idle | ||||
|     -1: "idle",  # DUPLICATED | ||||
|     # P1 returns 255 for idle | ||||
|     255: "idle", # DUPLICATED | ||||
| } | ||||
|  | ||||
| CURRENT_STAGE_OPTIONS = list(set(CURRENT_STAGE_IDS.values())) # Conversion to set first removes the duplicates | ||||
|  | ||||
| GCODE_STATE_OPTIONS = [ | ||||
|     "failed", | ||||
|     "finish", | ||||
|     "idle", | ||||
|     "init", | ||||
|     "offline", | ||||
|     "pause", | ||||
|     "prepare", | ||||
|     "running", | ||||
|     "slicing", | ||||
|     "unknown" | ||||
| ] | ||||
|  | ||||
| SPEED_PROFILE = { | ||||
|     1: "silent", | ||||
|     2: "standard", | ||||
|     3: "sport", | ||||
|     4: "ludicrous" | ||||
| } | ||||
|  | ||||
| PRINT_TYPE_OPTIONS = { | ||||
|     "cloud", | ||||
|     "local", | ||||
|     "idle", | ||||
|     "system", | ||||
|     "unknown" | ||||
| } | ||||
|  | ||||
|  | ||||
| def load_dict(filename: str) -> dict: | ||||
|     with open(filename) as f: | ||||
|         return json.load(f); | ||||
|  | ||||
|  | ||||
| FILAMENT_NAMES = load_dict(Path(__file__).with_name('filaments.json')) | ||||
|  | ||||
| HMS_SEVERITY_LEVELS = { | ||||
|     "default": "unknown", | ||||
|     1: "fatal", | ||||
|     2: "serious", | ||||
|     3: "common", | ||||
|     4: "info" | ||||
| } | ||||
|  | ||||
| HMS_MODULES = { | ||||
|     "default": "unknown", | ||||
|     0x05: "mainboard", | ||||
|     0x0C: "xcam", | ||||
|     0x07: "ams", | ||||
|     0x08: "toolhead", | ||||
|     0x03: "mc" | ||||
| } | ||||
|  | ||||
| class SdcardState(Enum): | ||||
|     NO_SDCARD                           = 0x00000000, | ||||
|     HAS_SDCARD_NORMAL                   = 0x00000100, | ||||
|     HAS_SDCARD_ABNORMAL                 = 0x00000200, | ||||
|     SDCARD_STATE_NUM                    = 0x00000300, | ||||
|  | ||||
| class Home_Flag_Values(IntEnum): | ||||
|     X_AXIS                              = 0x00000001, | ||||
|     Y_AXIS                              = 0x00000002, | ||||
|     Z_AXIS                              = 0x00000004, | ||||
|     VOLTAGE220                          = 0x00000008, | ||||
|     XCAM_AUTO_RECOVERY_STEP_LOSS        = 0x00000010, | ||||
|     CAMERA_RECORDING                    = 0x00000020, | ||||
|     # Gap | ||||
|     AMS_CALIBRATE_REMAINING             = 0x00000080, | ||||
|     SD_CARD_PRESENT                     = 0x00000100, | ||||
|     SD_CARD_ABNORMAL                    = 0x00000200, | ||||
|     AMS_AUTO_SWITCH                     = 0x00000400, | ||||
|     # Gap | ||||
|     XCAM_ALLOW_PROMPT_SOUND             = 0x00020000, | ||||
|     WIRED_NETWORK                       = 0x00040000, | ||||
|     FILAMENT_TANGLE_DETECT_SUPPORTED    = 0x00080000, | ||||
|     FILAMENT_TANGLE_DETECTED            = 0x00100000, | ||||
|     SUPPORTS_MOTOR_CALIBRATION          = 0x00200000, | ||||
|     # Gap | ||||
|     DOOR_OPEN                           = 0x00800000, | ||||
|     # Gap | ||||
|     INSTALLED_PLUS                      = 0x04000000, | ||||
|     SUPPORTED_PLUS                      = 0x08000000, | ||||
|     # Gap | ||||
|  | ||||
| class BambuUrl(Enum): | ||||
|     LOGIN = 1, | ||||
|     TFA_LOGIN = 2, | ||||
|     EMAIL_CODE = 3, | ||||
|     BIND = 4, | ||||
|     SLICER_SETTINGS = 5, | ||||
|     TASKS = 6, | ||||
|     PROJECTS = 7, | ||||
|  | ||||
| BAMBU_URL = { | ||||
|     BambuUrl.LOGIN: 'https://api.bambulab.com/v1/user-service/user/login', | ||||
|     BambuUrl.TFA_LOGIN: 'https://bambulab.com/api/sign-in/tfa', | ||||
|     BambuUrl.EMAIL_CODE: 'https://api.bambulab.com/v1/user-service/user/sendemail/code', | ||||
|     BambuUrl.BIND: 'https://api.bambulab.com/v1/iot-service/api/user/bind', | ||||
|     BambuUrl.SLICER_SETTINGS: 'https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=1.10.0.89', | ||||
|     BambuUrl.TASKS: 'https://api.bambulab.com/v1/user-service/my/tasks', | ||||
|     BambuUrl.PROJECTS: 'https://api.bambulab.com/v1/iot-service/api/user/project', | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| { | ||||
|     "GFA00": "Bambu PLA Basic", | ||||
|     "GFA01": "Bambu PLA Matte", | ||||
|     "GFA02": "Bambu PLA Metal", | ||||
|     "GFA05": "Bambu PLA Silk", | ||||
|     "GFA07": "Bambu PLA Marble", | ||||
|     "GFA08": "Bambu PLA Sparkle", | ||||
|     "GFA09": "Bambu PLA Tough", | ||||
|     "GFA11": "Bambu PLA Aero", | ||||
|     "GFA12": "Bambu PLA Glow", | ||||
|     "GFA13": "Bambu PLA Dynamic", | ||||
|     "GFA15": "Bambu PLA Galaxy", | ||||
|     "GFA50": "Bambu PLA-CF", | ||||
|     "GFB00": "Bambu ABS", | ||||
|     "GFB01": "Bambu ASA", | ||||
|     "GFB02": "Bambu ASA-Aero", | ||||
|     "GFB50": "Bambu ABS-GF", | ||||
|     "GFB51": "Bambu ASA-CF", | ||||
|     "GFB60": "PolyLite ABS", | ||||
|     "GFB61": "PolyLite ASA", | ||||
|     "GFB98": "Generic ASA", | ||||
|     "GFB99": "Generic ABS", | ||||
|     "GFC00": "Bambu PC", | ||||
|     "GFC99": "Generic PC", | ||||
|     "GFG00": "Bambu PETG Basic", | ||||
|     "GFG01": "Bambu PETG Translucent", | ||||
|     "GFG02": "Bambu PETG HF", | ||||
|     "GFG50": "Bambu PETG-CF", | ||||
|     "GFG60": "PolyLite PETG", | ||||
|     "GFG96": "Generic PETG HF", | ||||
|     "GFG97": "Generic PCTG", | ||||
|     "GFG98": "Generic PETG-CF", | ||||
|     "GFG99": "Generic PETG", | ||||
|     "GFL00": "PolyLite PLA", | ||||
|     "GFL01": "PolyTerra PLA", | ||||
|     "GFL03": "eSUN PLA+", | ||||
|     "GFL04": "Overture PLA", | ||||
|     "GFL05": "Overture Matte PLA", | ||||
|     "GFL06": "Fiberon PETG-ESD", | ||||
|     "GFL50": "Fiberon PA6-CF", | ||||
|     "GFL51": "Fiberon PA6-GF", | ||||
|     "GFL52": "Fiberon PA12-CF", | ||||
|     "GFL53": "Fiberon PA612-CF", | ||||
|     "GFL54": "Fiberon PET-CF", | ||||
|     "GFL55": "Fiberon PETG-rCF", | ||||
|     "GFL95": "Generic PLA High Speed", | ||||
|     "GFL96": "Generic PLA Silk", | ||||
|     "GFL98": "Generic PLA-CF", | ||||
|     "GFL99": "Generic PLA", | ||||
|     "GFN03": "Bambu PA-CF", | ||||
|     "GFN04": "Bambu PAHT-CF", | ||||
|     "GFN05": "Bambu PA6-CF", | ||||
|     "GFN06": "Bambu PPA-CF", | ||||
|     "GFN08": "Bambu PA6-GF", | ||||
|     "GFN96": "Generic PPA-GF", | ||||
|     "GFN97": "Generic PPA-CF", | ||||
|     "GFN98": "Generic PA-CF", | ||||
|     "GFN99": "Generic PA", | ||||
|     "GFP95": "Generic PP-GF", | ||||
|     "GFP96": "Generic PP-CF", | ||||
|     "GFP97": "Generic PP", | ||||
|     "GFP98": "Generic PE-CF", | ||||
|     "GFP99": "Generic PE", | ||||
|     "GFR98": "Generic PHA", | ||||
|     "GFR99": "Generic EVA", | ||||
|     "GFS00": "Bambu Support W", | ||||
|     "GFS01": "Bambu Support G", | ||||
|     "GFS02": "Bambu Support For PLA", | ||||
|     "GFS03": "Bambu Support For PA/PET", | ||||
|     "GFS04": "Bambu PVA", | ||||
|     "GFS05": "Bambu Support For PLA/PETG", | ||||
|     "GFS06": "Bambu Support for ABS", | ||||
|     "GFS97": "Generic BVOH", | ||||
|     "GFS98": "Generic HIPS", | ||||
|     "GFS99": "Generic PVA", | ||||
|     "GFT01": "Bambu PET-CF", | ||||
|     "GFT02": "Bambu PPS-CF", | ||||
|     "GFT97": "Generic PPS", | ||||
|     "GFT98": "Generic PPS-CF", | ||||
|     "GFU00": "Bambu TPU 95A HF", | ||||
|     "GFU01": "Bambu TPU 95A", | ||||
|     "GFU02": "Bambu TPU for AMS", | ||||
|     "GFU98": "Generic TPU for AMS", | ||||
|     "GFU99": "Generic TPU" | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,248 +0,0 @@ | ||||
| import math | ||||
| from datetime import datetime, timedelta | ||||
|  | ||||
| from .const import ( | ||||
|     CURRENT_STAGE_IDS, | ||||
|     SPEED_PROFILE, | ||||
|     FILAMENT_NAMES, | ||||
|     HMS_ERRORS, | ||||
|     HMS_AMS_ERRORS, | ||||
|     PRINT_ERROR_ERRORS, | ||||
|     HMS_SEVERITY_LEVELS, | ||||
|     HMS_MODULES, | ||||
|     LOGGER, | ||||
|     BAMBU_URL, | ||||
|     FansEnum, | ||||
|     TempEnum | ||||
| ) | ||||
| from .commands import SEND_GCODE_TEMPLATE | ||||
|  | ||||
|  | ||||
| def search(lst, predicate, default={}): | ||||
|     """Search an array for a string""" | ||||
|     for item in lst: | ||||
|         if predicate(item): | ||||
|             return item | ||||
|     return default | ||||
|  | ||||
|  | ||||
| def fan_percentage(speed): | ||||
|     """Converts a fan speed to percentage""" | ||||
|     if not speed: | ||||
|         return 0 | ||||
|     percentage = (int(speed) / 15) * 100 | ||||
|     return round(percentage / 10) * 10 | ||||
|  | ||||
|  | ||||
| def fan_percentage_to_gcode(fan: FansEnum, percentage: int): | ||||
|     """Converts a fan speed percentage to the gcode command to set that""" | ||||
|     if fan == FansEnum.PART_COOLING: | ||||
|         fanString = "P1" | ||||
|     elif fan == FansEnum.AUXILIARY: | ||||
|         fanString = "P2" | ||||
|     elif fan == FansEnum.CHAMBER: | ||||
|         fanString = "P3" | ||||
|  | ||||
|     percentage = round(percentage / 10) * 10 | ||||
|     speed = math.ceil(255 * percentage / 100) | ||||
|     command = SEND_GCODE_TEMPLATE | ||||
|     command['print']['param'] = f"M106 {fanString} S{speed}\n" | ||||
|     return command | ||||
|  | ||||
|  | ||||
| def set_temperature_to_gcode(temp: TempEnum, temperature: int): | ||||
|     """Converts a temperature to the gcode command to set that""" | ||||
|     if temp == TempEnum.NOZZLE: | ||||
|         tempCommand = "M104" | ||||
|     elif temp == TempEnum.HEATBED: | ||||
|         tempCommand = "M140" | ||||
|  | ||||
|     command = SEND_GCODE_TEMPLATE | ||||
|     command['print']['param'] = f"{tempCommand} S{temperature}\n" | ||||
|     return command | ||||
|  | ||||
|  | ||||
| def to_whole(number): | ||||
|     if not number: | ||||
|         return 0 | ||||
|     return round(number) | ||||
|  | ||||
|  | ||||
| def get_filament_name(idx, custom_filaments: dict): | ||||
|     """Converts a filament idx to a human-readable name""" | ||||
|     result = FILAMENT_NAMES.get(idx, "unknown") | ||||
|     if result == "unknown" and idx != "": | ||||
|         result = custom_filaments.get(idx, "unknown") | ||||
|     # if result == "unknown" and idx != "": | ||||
|     #     LOGGER.debug(f"UNKNOWN FILAMENT IDX: '{idx}'") | ||||
|     return result | ||||
|  | ||||
|  | ||||
| def get_speed_name(id): | ||||
|     """Return the human-readable name for a speed id""" | ||||
|     return SPEED_PROFILE.get(int(id), "standard") | ||||
|  | ||||
|  | ||||
| def get_current_stage(id) -> str: | ||||
|     """Return the human-readable description for a stage action""" | ||||
|     return CURRENT_STAGE_IDS.get(int(id), "unknown") | ||||
|  | ||||
|  | ||||
| def get_HMS_error_text(hms_code: str): | ||||
|     """Return the human-readable description for an HMS error""" | ||||
|  | ||||
|     ams_code = get_generic_AMS_HMS_error_code(hms_code) | ||||
|     ams_error = HMS_AMS_ERRORS.get(ams_code, "") | ||||
|     if ams_error != "": | ||||
|         # 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error | ||||
|         ams_index = int(hms_code[3:4], 16) + 1 | ||||
|         ams_slot = int(hms_code[6:7], 16) + 1 | ||||
|         ams_error = ams_error.replace('AMS1', f"AMS{ams_index}") | ||||
|         ams_error = ams_error.replace('slot 1', f"slot {ams_slot}") | ||||
|         return ams_error | ||||
|  | ||||
|     return HMS_ERRORS.get(hms_code, "unknown") | ||||
|  | ||||
|  | ||||
| def get_print_error_text(print_error_code: str): | ||||
|     """Return the human-readable description for a print error""" | ||||
|  | ||||
|     hex_conversion = f'0{int(print_error_code):x}' | ||||
|     print_error_code = hex_conversion[slice(0,4,1)] + "_" + hex_conversion[slice(4,8,1)] | ||||
|     print_error = PRINT_ERROR_ERRORS.get(print_error_code.upper(), "") | ||||
|     if print_error != "": | ||||
|         return print_error | ||||
|  | ||||
|     return PRINT_ERROR_ERRORS.get(print_error_code, "unknown") | ||||
|  | ||||
|  | ||||
| def get_HMS_severity(code: int) -> str: | ||||
|     uint_code = code >> 16 | ||||
|     if code > 0 and uint_code in HMS_SEVERITY_LEVELS: | ||||
|         return HMS_SEVERITY_LEVELS[uint_code] | ||||
|     return HMS_SEVERITY_LEVELS["default"] | ||||
|  | ||||
|  | ||||
| def get_HMS_module(attr: int) -> str: | ||||
|     uint_attr = (attr >> 24) & 0xFF | ||||
|     if attr > 0 and uint_attr in HMS_MODULES: | ||||
|         return HMS_MODULES[uint_attr] | ||||
|     return HMS_MODULES["default"] | ||||
|  | ||||
|  | ||||
| def get_generic_AMS_HMS_error_code(hms_code: str): | ||||
|     code1 = int(hms_code[0:4], 16) | ||||
|     code2 = int(hms_code[5:9], 16) | ||||
|     code3 = int(hms_code[10:14], 16) | ||||
|     code4 = int(hms_code[15:19], 16) | ||||
|  | ||||
|     # 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error | ||||
|     ams_code = f"{code1 & 0xFFF8:0>4X}_{code2 & 0xF8FF:0>4X}_{code3:0>4X}_{code4:0>4X}" | ||||
|     ams_error = HMS_AMS_ERRORS.get(ams_code, "") | ||||
|     if ams_error != "": | ||||
|         return ams_code | ||||
|  | ||||
|     return f"{code1:0>4X}_{code2:0>4X}_{code3:0>4X}_{code4:0>4X}" | ||||
|  | ||||
|  | ||||
| def get_printer_type(modules, default): | ||||
|     # Known possible values: | ||||
|     #  | ||||
|     # A1/P1 printers are of the form: | ||||
|     # { | ||||
|     #     "name": "esp32", | ||||
|     #     "project_name": "C11", | ||||
|     #     "sw_ver": "01.07.23.47", | ||||
|     #     "hw_ver": "AP04", | ||||
|     #     "sn": "**REDACTED**", | ||||
|     #     "flag": 0 | ||||
|     # }, | ||||
|     # P1P    = AP04 / C11 | ||||
|     # P1S    = AP04 / C12 | ||||
|     # A1Mini = AP05 / N1 or AP04 / N1 or AP07 / N1 | ||||
|     # A1     = AP05 / N2S | ||||
|     # | ||||
|     # X1C printers are of the form: | ||||
|     # { | ||||
|     #     "hw_ver": "AP05", | ||||
|     #     "name": "rv1126", | ||||
|     #     "sn": "**REDACTED**", | ||||
|     #     "sw_ver": "00.00.28.55" | ||||
|     # }, | ||||
|     # X1C = AP05 | ||||
|     # | ||||
|     # X1E printers are of the form: | ||||
|     # { | ||||
|     #     "flag": 0, | ||||
|     #     "hw_ver": "AP02", | ||||
|     #     "name": "ap", | ||||
|     #     "sn": "**REDACTED**", | ||||
|     #     "sw_ver": "00.00.32.14" | ||||
|     # } | ||||
|     # X1E = AP02 | ||||
|  | ||||
|     apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0) | ||||
|     if len(apNode.keys()) > 1: | ||||
|         hw_ver = apNode['hw_ver'] | ||||
|         project_name = apNode.get('project_name', '') | ||||
|         if hw_ver == 'AP02': | ||||
|             return 'X1E' | ||||
|         elif project_name == 'N1': | ||||
|             return 'A1MINI' | ||||
|         elif hw_ver == 'AP04': | ||||
|             if project_name == 'C11': | ||||
|                 return 'P1P' | ||||
|             if project_name == 'C12': | ||||
|                 return 'P1S' | ||||
|         elif hw_ver == 'AP05': | ||||
|             if project_name == 'N2S': | ||||
|                 return 'A1' | ||||
|             if project_name == '': | ||||
|                 return 'X1C' | ||||
|         LOGGER.debug(f"UNKNOWN DEVICE: hw_ver='{hw_ver}' / project_name='{project_name}'") | ||||
|     return default | ||||
|  | ||||
|  | ||||
| def get_hw_version(modules, default): | ||||
|     """Retrieve hardware version of printer""" | ||||
|     apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0) | ||||
|     if len(apNode.keys()) > 1: | ||||
|         return apNode.get("hw_ver") | ||||
|     return default | ||||
|  | ||||
|  | ||||
| def get_sw_version(modules, default): | ||||
|     """Retrieve software version of printer""" | ||||
|     ota = search(modules, lambda x: x.get('name', "") == "ota") | ||||
|     if len(ota.keys()) > 1: | ||||
|         return ota.get("sw_ver") | ||||
|     return default | ||||
|  | ||||
|  | ||||
| def get_start_time(timestamp): | ||||
|     """Return start time of a print""" | ||||
|     if timestamp == 0: | ||||
|         return None | ||||
|     return datetime.fromtimestamp(timestamp) | ||||
|  | ||||
|  | ||||
| def get_end_time(remaining_time): | ||||
|     """Calculate the end time of a print""" | ||||
|     end_time = round_minute(datetime.now() + timedelta(minutes=remaining_time)) | ||||
|     return end_time | ||||
|  | ||||
|  | ||||
| def round_minute(date: datetime = None, round_to: int = 1): | ||||
|     """ Round datetime object to minutes""" | ||||
|     if not date: | ||||
|         date = datetime.now() | ||||
|     date = date.replace(second=0, microsecond=0) | ||||
|     delta = date.minute % round_to | ||||
|     return date.replace(minute=date.minute - delta) | ||||
|  | ||||
|  | ||||
| def get_Url(url: str, region: str): | ||||
|     urlstr = BAMBU_URL[url] | ||||
|     if region == "China": | ||||
|         urlstr = urlstr.replace('.com', '.cn') | ||||
|     return urlstr | ||||
| @@ -25,7 +25,7 @@ class IdleState(APrinterState): | ||||
|         filesystem_root = ( | ||||
|             "file:///mnt/sdcard/" | ||||
|             if self._printer._settings.get(["device_type"]) in ["X1", "X1C"] | ||||
|             else "file:///sdcard/" | ||||
|             else "file:///" | ||||
|         ) | ||||
|  | ||||
|         print_command = { | ||||
| @@ -49,7 +49,7 @@ class IdleState(APrinterState): | ||||
|                 ), | ||||
|                 "layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]), | ||||
|                 "use_ams": self._printer._settings.get_boolean(["use_ams"]), | ||||
|                 "ams_mapping": self._printer._settings.get(["ams_mapping"]), | ||||
|                 "ams_mapping": "", | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ if TYPE_CHECKING: | ||||
|  | ||||
| import threading | ||||
|  | ||||
| import octoprint_bambu_printer.printer.pybambu.commands | ||||
| import pybambu.commands | ||||
| from octoprint.util import RepeatedTimer | ||||
|  | ||||
| from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | ||||
| @@ -37,14 +37,14 @@ class PausedState(APrinterState): | ||||
|  | ||||
|     def start_new_print(self): | ||||
|         if self._printer.bambu_client.connected: | ||||
|             if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.RESUME): | ||||
|             if self._printer.bambu_client.publish(pybambu.commands.RESUME): | ||||
|                 self._log.info("print resumed") | ||||
|             else: | ||||
|                 self._log.info("print resume failed") | ||||
|  | ||||
|     def cancel_print(self): | ||||
|         if self._printer.bambu_client.connected: | ||||
|             if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.STOP): | ||||
|             if self._printer.bambu_client.publish(pybambu.commands.STOP): | ||||
|                 self._log.info("print cancelled") | ||||
|                 self._printer.finalize_print_job() | ||||
|             else: | ||||
|   | ||||
| @@ -10,9 +10,9 @@ if TYPE_CHECKING: | ||||
|  | ||||
| import threading | ||||
|  | ||||
| import octoprint_bambu_printer.printer.pybambu | ||||
| import octoprint_bambu_printer.printer.pybambu.models | ||||
| import octoprint_bambu_printer.printer.pybambu.commands | ||||
| import pybambu | ||||
| import pybambu.models | ||||
| import pybambu.commands | ||||
|  | ||||
| from octoprint_bambu_printer.printer.print_job import PrintJob | ||||
| from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | ||||
| @@ -22,7 +22,7 @@ class PrintingState(APrinterState): | ||||
|  | ||||
|     def __init__(self, printer: BambuVirtualPrinter) -> None: | ||||
|         super().__init__(printer) | ||||
|         self._printer.current_print_job = None | ||||
|         self._current_print_job = None | ||||
|         self._is_printing = False | ||||
|         self._sd_printing_thread = None | ||||
|  | ||||
| @@ -40,15 +40,12 @@ class PrintingState(APrinterState): | ||||
|         self._printer.current_print_job = None | ||||
|  | ||||
|     def _start_worker_thread(self): | ||||
|         self._is_printing = True | ||||
|         if self._sd_printing_thread is None: | ||||
|             self._is_printing = True | ||||
|             self._sd_printing_thread = threading.Thread(target=self._printing_worker) | ||||
|             self._sd_printing_thread.start() | ||||
|         else: | ||||
|             self._sd_printing_thread.join() | ||||
|  | ||||
|     def _printing_worker(self): | ||||
|         self._log.debug(f"_printing_worker before while loop: {self._printer.current_print_job}") | ||||
|         while ( | ||||
|             self._is_printing | ||||
|             and self._printer.current_print_job is not None | ||||
| @@ -58,7 +55,6 @@ class PrintingState(APrinterState): | ||||
|             self._printer.report_print_job_status() | ||||
|             time.sleep(3) | ||||
|  | ||||
|         self._log.debug(f"_printing_worker after while loop: {self._printer.current_print_job}") | ||||
|         self.update_print_job_info() | ||||
|         if ( | ||||
|             self._printer.current_print_job is not None | ||||
| @@ -68,34 +64,30 @@ class PrintingState(APrinterState): | ||||
|  | ||||
|     def update_print_job_info(self): | ||||
|         print_job_info = self._printer.bambu_client.get_device().print_job | ||||
|         subtask_name: str = print_job_info.subtask_name | ||||
|         gcode_file: str = print_job_info.gcode_file | ||||
|  | ||||
|         self._log.debug(f"update_print_job_info: {print_job_info}") | ||||
|  | ||||
|         project_file_info = self._printer.project_files.get_file_by_name(subtask_name) or self._printer.project_files.get_file_by_name(gcode_file) | ||||
|         task_name: str = print_job_info.subtask_name | ||||
|         project_file_info = self._printer.project_files.get_file_by_stem( | ||||
|             task_name, [".gcode", ".3mf"] | ||||
|         ) | ||||
|         if project_file_info is None: | ||||
|             self._log.debug(f"No 3mf file found for {print_job_info}") | ||||
|             self._printer.current_print_job = None | ||||
|             self._current_print_job = None | ||||
|             self._printer.change_state(self._printer._state_idle) | ||||
|             return | ||||
|  | ||||
|         progress = print_job_info.print_percentage | ||||
|         if print_job_info.gcode_state == "PREPARE" and progress == 100: | ||||
|             progress = 0 | ||||
|         self._printer.current_print_job = PrintJob(project_file_info, progress, print_job_info.remaining_time, print_job_info.current_layer, print_job_info.total_layers) | ||||
|         self._printer.current_print_job = PrintJob(project_file_info, progress) | ||||
|         self._printer.select_project_file(project_file_info.path.as_posix()) | ||||
|  | ||||
|     def pause_print(self): | ||||
|         if self._printer.bambu_client.connected: | ||||
|             if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.PAUSE): | ||||
|             if self._printer.bambu_client.publish(pybambu.commands.PAUSE): | ||||
|                 self._log.info("print paused") | ||||
|             else: | ||||
|                 self._log.info("print pause failed") | ||||
|  | ||||
|     def cancel_print(self): | ||||
|         if self._printer.bambu_client.connected: | ||||
|             if self._printer.bambu_client.publish(octoprint_bambu_printer.printer.pybambu.commands.STOP): | ||||
|             if self._printer.bambu_client.publish(pybambu.commands.STOP): | ||||
|                 self._log.info("print cancelled") | ||||
|                 self._printer.finalize_print_job() | ||||
|             else: | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| #sidebar_plugin_bambu_printer div.well { | ||||
|     min-height: 70px; | ||||
| } | ||||
|  | ||||
| #sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid { | ||||
|     padding-top: 10px; | ||||
| } | ||||
|  | ||||
| #sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid.active { | ||||
|     border: 2px solid; | ||||
|     -webkit-border-radius: 4px; | ||||
|     -moz-border-radius: 4px; | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| #bambu_printer_print_options div.well { | ||||
|     min-height: 60px; | ||||
| } | ||||
|  | ||||
| #bambu_printer_print_options div.modal-body { | ||||
|     overflow: inherit !important; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -15,69 +15,18 @@ $(function () { | ||||
|         self.accessViewModel = parameters[3]; | ||||
|         self.timelapseViewModel = parameters[4]; | ||||
|  | ||||
|         self.use_ams = true; | ||||
|         self.ams_mapping = ko.observableArray([]); | ||||
|  | ||||
|         self.job_info = ko.observable(); | ||||
|  | ||||
|         self.auth_type = ko.observable(""); | ||||
|  | ||||
|         self.show_password = ko.pureComputed(function(){ | ||||
|             return self.settingsViewModel.settings.plugins.bambu_printer.auth_token() === ''; | ||||
|         }); | ||||
|  | ||||
|         self.show_verification = ko.pureComputed(function(){ | ||||
|             return self.auth_type() !== ''; | ||||
|         }); | ||||
|  | ||||
|         self.ams_mapping_computed = function(){ | ||||
|             var output_list = []; | ||||
|             var index = 0; | ||||
|  | ||||
|             ko.utils.arrayForEach(self.settingsViewModel.settings.plugins.bambu_printer.ams_data(), function(item){ | ||||
|                 if(item){ | ||||
|                     output_list = output_list.concat(item.tray()); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             ko.utils.arrayForEach(output_list, function(item){ | ||||
|                 item["index"] = ko.observable(index); | ||||
|                 index++; | ||||
|             }); | ||||
|  | ||||
|             return output_list; | ||||
|         }; | ||||
|  | ||||
|         self.getAuthToken = function (data) { | ||||
|             self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); | ||||
|             self.auth_type(""); | ||||
|             OctoPrint.simpleApiCommand("bambu_printer", "register", { | ||||
|                 "email": self.settingsViewModel.settings.plugins.bambu_printer.email(), | ||||
|                 "password": $("#bambu_cloud_password").val(), | ||||
|                 "region": self.settingsViewModel.settings.plugins.bambu_printer.region(), | ||||
|                 "auth_token": self.settingsViewModel.settings.plugins.bambu_printer.auth_token() | ||||
|             }) | ||||
|                 .done(function (response) { | ||||
|                     self.auth_type(response.auth_response); | ||||
|                 }); | ||||
|         }; | ||||
|  | ||||
|         self.verifyCode = function (data) { | ||||
|             self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); | ||||
|             OctoPrint.simpleApiCommand("bambu_printer", "verify", { | ||||
|                 "password": $("#bambu_cloud_verify_code").val(), | ||||
|                 "auth_type": self.auth_type(), | ||||
|             }) | ||||
|                 .done(function (response) { | ||||
|                     console.log(response); | ||||
|                     if (response.auth_token) { | ||||
|                     self.settingsViewModel.settings.plugins.bambu_printer.auth_token(response.auth_token); | ||||
|                     self.settingsViewModel.settings.plugins.bambu_printer.username(response.username); | ||||
|                         self.auth_type(""); | ||||
|                     } else if (response.error) { | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); | ||||
|                         $("#bambu_cloud_verify_code").val(""); | ||||
|                     } | ||||
|                 }); | ||||
|         }; | ||||
|  | ||||
| @@ -119,72 +68,81 @@ $(function () { | ||||
|             } | ||||
|  | ||||
|             if (data.files !== undefined) { | ||||
|                 console.log(data.files); | ||||
|                 self.listHelper.updateItems(data.files); | ||||
|                 self.listHelper.resetPage(); | ||||
|             } | ||||
|  | ||||
|             if (data.job_info !== undefined) { | ||||
|                 self.job_info(data.job_info); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         self.onBeforeBinding = function () { | ||||
|             $('#bambu_timelapse').appendTo("#timelapse"); | ||||
|         }; | ||||
|  | ||||
|         self.onAfterBinding = function () { | ||||
|             console.log(self.ams_mapping_computed()); | ||||
|         }; | ||||
|  | ||||
|         self.showTimelapseThumbnail = function(data) { | ||||
|             $("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail); | ||||
|             $("#bambu_printer_timelapse_preview").modal('show'); | ||||
|         }; | ||||
|  | ||||
|         self.onBeforePrintStart = function(start_print_command, data) { | ||||
|             self.ams_mapping(self.ams_mapping_computed()); | ||||
|             self.start_print_command = start_print_command; | ||||
|             self.use_ams = self.settingsViewModel.settings.plugins.bambu_printer.use_ams(); | ||||
|             // prevent starting locally stored files, once data is added to core OctoPrint this | ||||
|             // could be adjusted to include additional processing like get sliced file's | ||||
|             // spool assignments and colors from plate_#.json inside 3mf file. | ||||
|             if(data && data.origin !== "sdcard") { | ||||
|                 return false; | ||||
|         /*$('#files div.upload-buttons > span.fileinput-button:first, #files div.folder-button').remove(); | ||||
|         $('#files div.upload-buttons > span.fileinput-button:first').removeClass('span6').addClass('input-block-level'); | ||||
|  | ||||
|         self.onBeforePrintStart = function(start_print_command) { | ||||
|             let confirmation_html = '' + | ||||
|                 '            <div class="row-fluid form-vertical">\n' + | ||||
|                 '                <div class="control-group">\n' + | ||||
|                 '                    <label class="control-label">' + gettext("Plate Number") + '</label>\n' + | ||||
|                 '                    <div class="controls">\n' + | ||||
|                 '                        <input type="number" min="1" value="1" id="bambu_printer_plate_number" class="input-mini">\n' + | ||||
|                 '                    </div>\n' + | ||||
|                 '                </div>\n' + | ||||
|                 '            </div>'; | ||||
|  | ||||
|             if(!self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options()){ | ||||
|                 confirmation_html += '\n' + | ||||
|                     '            <div class="row-fluid">\n' + | ||||
|                     '                <div class="span6">\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_timelapse" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.timelapse()) ? ' checked' : '') + '> ' + gettext("Enable timelapse") + '</label>\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_bed_leveling" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.bed_leveling()) ? ' checked' : '') + '> ' + gettext("Enable bed leveling") + '</label>\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_flow_cali" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.flow_cali()) ? ' checked' : '') + '> ' + gettext("Enable flow calibration") + '</label>\n' + | ||||
|                     '                </div>\n' + | ||||
|                     '                <div class="span6">\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_vibration_cali" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali()) ? ' checked' : '') + '> ' + gettext("Enable vibration calibration") + '</label>\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_layer_inspect" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect()) ? ' checked' : '') + '> ' + gettext("Enable first layer inspection") + '</label>\n' + | ||||
|                     '                    <label class="checkbox"><input id="bambu_printer_use_ams" type="checkbox"' + ((self.settingsViewModel.settings.plugins.bambu_printer.use_ams()) ? ' checked' : '') + '> ' + gettext("Use AMS") + '</label>\n' + | ||||
|                     '                </div>\n' + | ||||
|                     '            </div>\n'; | ||||
|             } | ||||
|             $("#bambu_printer_print_options").modal('show'); | ||||
|             return false; | ||||
|         }; | ||||
|  | ||||
|         self.toggle_spool_active = function(data) { | ||||
|             if(data.index() >= 0){ | ||||
|                 data.original_index = ko.observable(data.index()); | ||||
|                 data.index(-1); | ||||
|             } else { | ||||
|                 data.index(data.original_index()); | ||||
|             showConfirmationDialog({ | ||||
|                 title: "Bambu Print Options", | ||||
|                 html: confirmation_html, | ||||
|                 cancel: gettext("Cancel"), | ||||
|                 proceed: [gettext("Print"), gettext("Always")], | ||||
|                 onproceed: function (idx) { | ||||
|                     if(idx === 1){ | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.timelapse($('#bambu_printer_timelapse').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.bed_leveling($('#bambu_printer_bed_leveling').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.flow_cali($('#bambu_printer_flow_cali').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali($('#bambu_printer_vibration_cali').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect($('#bambu_printer_layer_inspect').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.use_ams($('#bambu_printer_use_ams').is(':checked')); | ||||
|                         self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options(true); | ||||
|                         self.settingsViewModel.saveData(); | ||||
|                     } | ||||
|         }; | ||||
|  | ||||
|         self.cancel_print_options = function() { | ||||
|             self.settingsViewModel.settings.plugins.bambu_printer.use_ams(self.use_ams); | ||||
|             $("#bambu_printer_print_options").modal('hide'); | ||||
|         }; | ||||
|  | ||||
|         self.accept_print_options = function() { | ||||
|             console.log("starting print!!!!"); | ||||
|             console.log(self.ams_mapping()); | ||||
|             $("#bambu_printer_print_options").modal('hide'); | ||||
|             var flattened_ams_mapping = ko.utils.arrayMap(self.ams_mapping(), function(item) { | ||||
|                 return item.index(); | ||||
|                     // replace this with our own print command API call? | ||||
|                     start_print_command(); | ||||
|                 }, | ||||
|                 nofade: true | ||||
|             }); | ||||
|             self.settingsViewModel.settings.plugins.bambu_printer.ams_mapping(flattened_ams_mapping); | ||||
|             self.settingsViewModel.saveData(undefined, self.start_print_command); | ||||
|             // self.settingsViewModel.saveData(); | ||||
|         }; | ||||
|             return false; | ||||
|         };*/ | ||||
|     } | ||||
|  | ||||
|     OCTOPRINT_VIEWMODELS.push({ | ||||
|         construct: Bambu_printerViewModel, | ||||
|         // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... | ||||
|         dependencies: ["settingsViewModel", "filesViewModel", "loginStateViewModel", "accessViewModel", "timelapseViewModel"], | ||||
|         elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse", "#sidebar_plugin_bambu_printer"] | ||||
|         // Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ... | ||||
|         elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse"] | ||||
|     }); | ||||
| }); | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,490 +0,0 @@ | ||||
| // knockout-sortable 1.2.0 | (c) 2019 Ryan Niemeyer |  http://www.opensource.org/licenses/mit-license | ||||
| ;(function(factory) { | ||||
|     if (typeof define === "function" && define.amd) { | ||||
|         // AMD anonymous module | ||||
|         define(["knockout", "jquery", "jquery-ui/ui/widgets/sortable", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable"], factory); | ||||
|     } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { | ||||
|         // CommonJS module | ||||
|         var ko = require("knockout"), | ||||
|             jQuery = require("jquery"); | ||||
|         require("jquery-ui/ui/widgets/sortable"); | ||||
|         require("jquery-ui/ui/widgets/draggable"); | ||||
|         require("jquery-ui/ui/widgets/droppable"); | ||||
|         factory(ko, jQuery); | ||||
|     } else { | ||||
|         // No module loader (plain <script> tag) - put directly in global namespace | ||||
|         factory(window.ko, window.jQuery); | ||||
|     } | ||||
| })(function(ko, $) { | ||||
|     var ITEMKEY = "ko_sortItem", | ||||
|         INDEXKEY = "ko_sourceIndex", | ||||
|         LISTKEY = "ko_sortList", | ||||
|         PARENTKEY = "ko_parentList", | ||||
|         DRAGKEY = "ko_dragItem", | ||||
|         unwrap = ko.utils.unwrapObservable, | ||||
|         dataGet = ko.utils.domData.get, | ||||
|         dataSet = ko.utils.domData.set, | ||||
|         version = $.ui && $.ui.version, | ||||
|         //1.8.24 included a fix for how events were triggered in nested sortables. indexOf checks will fail if version starts with that value (0 vs. -1) | ||||
|         hasNestedSortableFix = version && version.indexOf("1.6.") && version.indexOf("1.7.") && (version.indexOf("1.8.") || version === "1.8.24"); | ||||
|  | ||||
|     //internal afterRender that adds meta-data to children | ||||
|     var addMetaDataAfterRender = function(elements, data) { | ||||
|         ko.utils.arrayForEach(elements, function(element) { | ||||
|             if (element.nodeType === 1) { | ||||
|                 dataSet(element, ITEMKEY, data); | ||||
|                 dataSet(element, PARENTKEY, dataGet(element.parentNode, LISTKEY)); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     //prepare the proper options for the template binding | ||||
|     var prepareTemplateOptions = function(valueAccessor, dataName) { | ||||
|         var result = {}, | ||||
|             options = {}, | ||||
|             actualAfterRender; | ||||
|  | ||||
|         //build our options to pass to the template engine | ||||
|         if (ko.utils.peekObservable(valueAccessor()).data) { | ||||
|             options = unwrap(valueAccessor() || {}); | ||||
|             result[dataName] = options.data; | ||||
|             if (options.hasOwnProperty("template")) { | ||||
|                 result.name = options.template; | ||||
|             } | ||||
|         } else { | ||||
|             result[dataName] = valueAccessor(); | ||||
|         } | ||||
|  | ||||
|         ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions", "nodes"], function (option) { | ||||
|             if (options.hasOwnProperty(option)) { | ||||
|                 result[option] = options[option]; | ||||
|             } else if (ko.bindingHandlers.sortable.hasOwnProperty(option)) { | ||||
|                 result[option] = ko.bindingHandlers.sortable[option]; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //use an afterRender function to add meta-data | ||||
|         if (dataName === "foreach") { | ||||
|             if (result.afterRender) { | ||||
|                 //wrap the existing function, if it was passed | ||||
|                 actualAfterRender = result.afterRender; | ||||
|                 result.afterRender = function(element, data) { | ||||
|                     addMetaDataAfterRender.call(data, element, data); | ||||
|                     actualAfterRender.call(data, element, data); | ||||
|                 }; | ||||
|             } else { | ||||
|                 result.afterRender = addMetaDataAfterRender; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //return options to pass to the template binding | ||||
|         return result; | ||||
|     }; | ||||
|  | ||||
|     var updateIndexFromDestroyedItems = function(index, items) { | ||||
|         var unwrapped = unwrap(items); | ||||
|  | ||||
|         if (unwrapped) { | ||||
|             for (var i = 0; i <= index; i++) { | ||||
|                 //add one for every destroyed item we find before the targetIndex in the target array | ||||
|                 if (unwrapped[i] && unwrap(unwrapped[i]._destroy)) { | ||||
|                     index++; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return index; | ||||
|     }; | ||||
|  | ||||
|     //remove problematic leading/trailing whitespace from templates | ||||
|     var stripTemplateWhitespace = function(element, name) { | ||||
|         var templateSource, | ||||
|             templateElement; | ||||
|  | ||||
|         //process named templates | ||||
|         if (name) { | ||||
|             templateElement = document.getElementById(name); | ||||
|             if (templateElement) { | ||||
|                 templateSource = new ko.templateSources.domElement(templateElement); | ||||
|                 templateSource.text($.trim(templateSource.text())); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             //remove leading/trailing non-elements from anonymous templates | ||||
|             $(element).contents().each(function() { | ||||
|                 if (this && this.nodeType !== 1) { | ||||
|                     element.removeChild(this); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     //connect items with observableArrays | ||||
|     ko.bindingHandlers.sortable = { | ||||
|         init: function(element, valueAccessor, allBindingsAccessor, data, context) { | ||||
|             var $element = $(element), | ||||
|                 value = unwrap(valueAccessor()) || {}, | ||||
|                 templateOptions = prepareTemplateOptions(valueAccessor, "foreach"), | ||||
|                 sortable = {}, | ||||
|                 startActual, updateActual; | ||||
|  | ||||
|             stripTemplateWhitespace(element, templateOptions.name); | ||||
|  | ||||
|             //build a new object that has the global options with overrides from the binding | ||||
|             $.extend(true, sortable, ko.bindingHandlers.sortable); | ||||
|             if (value.options && sortable.options) { | ||||
|                 ko.utils.extend(sortable.options, value.options); | ||||
|                 delete value.options; | ||||
|             } | ||||
|             ko.utils.extend(sortable, value); | ||||
|  | ||||
|             //if allowDrop is an observable or a function, then execute it in a computed observable | ||||
|             if (sortable.connectClass && (ko.isObservable(sortable.allowDrop) || typeof sortable.allowDrop == "function")) { | ||||
|                 ko.computed({ | ||||
|                     read: function() { | ||||
|                         var value = unwrap(sortable.allowDrop), | ||||
|                             shouldAdd = typeof value == "function" ? value.call(this, templateOptions.foreach) : value; | ||||
|                         ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, shouldAdd); | ||||
|                     }, | ||||
|                     disposeWhenNodeIsRemoved: element | ||||
|                 }, this); | ||||
|             } else { | ||||
|                 ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop); | ||||
|             } | ||||
|  | ||||
|             //wrap the template binding | ||||
|             ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context); | ||||
|  | ||||
|             //keep a reference to start/update functions that might have been passed in | ||||
|             startActual = sortable.options.start; | ||||
|             updateActual = sortable.options.update; | ||||
|  | ||||
|             //ensure draggable table row cells maintain their width while dragging (unless a helper is provided) | ||||
|             if ( !sortable.options.helper ) { | ||||
|                 sortable.options.helper = function(e, ui) { | ||||
|                     if (ui.is("tr")) { | ||||
|                         ui.children().each(function() { | ||||
|                             $(this).width($(this).width()); | ||||
|                         }); | ||||
|                     } | ||||
|                     return ui; | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             //initialize sortable binding after template binding has rendered in update function | ||||
|             var createTimeout = setTimeout(function() { | ||||
|                 var dragItem; | ||||
|                 var originalReceive = sortable.options.receive; | ||||
|  | ||||
|                 $element.sortable(ko.utils.extend(sortable.options, { | ||||
|                     start: function(event, ui) { | ||||
|                         //track original index | ||||
|                         var el = ui.item[0]; | ||||
|                         dataSet(el, INDEXKEY, ko.utils.arrayIndexOf(ui.item.parent().children(), el)); | ||||
|  | ||||
|                         //make sure that fields have a chance to update model | ||||
|                         ui.item.find("input:focus").change(); | ||||
|                         if (startActual) { | ||||
|                             startActual.apply(this, arguments); | ||||
|                         } | ||||
|                     }, | ||||
|                     receive: function(event, ui) { | ||||
|                         //optionally apply an existing receive handler | ||||
|                         if (typeof originalReceive === "function") { | ||||
|                             originalReceive.call(this, event, ui); | ||||
|                         } | ||||
|  | ||||
|                         dragItem = dataGet(ui.item[0], DRAGKEY); | ||||
|                         if (dragItem) { | ||||
|                             //copy the model item, if a clone option is provided | ||||
|                             if (dragItem.clone) { | ||||
|                                 dragItem = dragItem.clone(); | ||||
|                             } | ||||
|  | ||||
|                             //configure a handler to potentially manipulate item before drop | ||||
|                             if (sortable.dragged) { | ||||
|                                 dragItem = sortable.dragged.call(this, dragItem, event, ui) || dragItem; | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     update: function(event, ui) { | ||||
|                         var sourceParent, targetParent, sourceIndex, targetIndex, arg, | ||||
|                             el = ui.item[0], | ||||
|                             parentEl = ui.item.parent()[0], | ||||
|                             item = dataGet(el, ITEMKEY) || dragItem; | ||||
|  | ||||
|                         if (!item) { | ||||
|                             $(el).remove(); | ||||
|                         } | ||||
|                         dragItem = null; | ||||
|  | ||||
|                         //make sure that moves only run once, as update fires on multiple containers | ||||
|                         if (item && (this === parentEl) || (!hasNestedSortableFix && $.contains(this, parentEl))) { | ||||
|                             //identify parents | ||||
|                             sourceParent = dataGet(el, PARENTKEY); | ||||
|                             sourceIndex = dataGet(el, INDEXKEY); | ||||
|                             targetParent = dataGet(el.parentNode, LISTKEY); | ||||
|                             targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el); | ||||
|  | ||||
|                             //take destroyed items into consideration | ||||
|                             if (!templateOptions.includeDestroyed) { | ||||
|                                 sourceIndex = updateIndexFromDestroyedItems(sourceIndex, sourceParent); | ||||
|                                 targetIndex = updateIndexFromDestroyedItems(targetIndex, targetParent); | ||||
|                             } | ||||
|  | ||||
|                             //build up args for the callbacks | ||||
|                             if (sortable.beforeMove || sortable.afterMove) { | ||||
|                                 arg = { | ||||
|                                     item: item, | ||||
|                                     sourceParent: sourceParent, | ||||
|                                     sourceParentNode: sourceParent && ui.sender || el.parentNode, | ||||
|                                     sourceIndex: sourceIndex, | ||||
|                                     targetParent: targetParent, | ||||
|                                     targetIndex: targetIndex, | ||||
|                                     cancelDrop: false | ||||
|                                 }; | ||||
|  | ||||
|                                 //execute the configured callback prior to actually moving items | ||||
|                                 if (sortable.beforeMove) { | ||||
|                                     sortable.beforeMove.call(this, arg, event, ui); | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             //call cancel on the correct list, so KO can take care of DOM manipulation | ||||
|                             if (sourceParent) { | ||||
|                                 $(sourceParent === targetParent ? this : ui.sender || this).sortable("cancel"); | ||||
|                             } | ||||
|                             //for a draggable item just remove the element | ||||
|                             else { | ||||
|                                 $(el).remove(); | ||||
|                             } | ||||
|  | ||||
|                             //if beforeMove told us to cancel, then we are done | ||||
|                             if (arg && arg.cancelDrop) { | ||||
|                                 return; | ||||
|                             } | ||||
|  | ||||
|                             //if the strategy option is unset or false, employ the order strategy involving removal and insertion of items | ||||
|                             if (!sortable.hasOwnProperty("strategyMove") || sortable.strategyMove === false) { | ||||
|                                 //do the actual move | ||||
|                                 if (targetIndex >= 0) { | ||||
|                                     if (sourceParent) { | ||||
|                                         sourceParent.splice(sourceIndex, 1); | ||||
|  | ||||
|                                         //if using deferred updates plugin, force updates | ||||
|                                         if (ko.processAllDeferredBindingUpdates) { | ||||
|                                             ko.processAllDeferredBindingUpdates(); | ||||
|                                         } | ||||
|  | ||||
|                                         //if using deferred updates on knockout 3.4, force updates | ||||
|                                         if (ko.options && ko.options.deferUpdates) { | ||||
|                                             ko.tasks.runEarly(); | ||||
|                                         } | ||||
|                                     } | ||||
|  | ||||
|                                     targetParent.splice(targetIndex, 0, item); | ||||
|                                 } | ||||
|  | ||||
|                                 //rendering is handled by manipulating the observableArray; ignore dropped element | ||||
|                                 dataSet(el, ITEMKEY, null); | ||||
|                             } | ||||
|                             else { //employ the strategy of moving items | ||||
|                                 if (targetIndex >= 0) { | ||||
|                                     if (sourceParent) { | ||||
|                                         if (sourceParent !== targetParent) { | ||||
|                                             // moving from one list to another | ||||
|  | ||||
|                                             sourceParent.splice(sourceIndex, 1); | ||||
|                                             targetParent.splice(targetIndex, 0, item); | ||||
|  | ||||
|                                             //rendering is handled by manipulating the observableArray; ignore dropped element | ||||
|                                             dataSet(el, ITEMKEY, null); | ||||
|                                             ui.item.remove(); | ||||
|                                         } | ||||
|                                         else { | ||||
|                                             // moving within same list | ||||
|                                             var underlyingList = unwrap(sourceParent); | ||||
|  | ||||
|                                             // notify 'beforeChange' subscribers | ||||
|                                             if (sourceParent.valueWillMutate) { | ||||
|                                                 sourceParent.valueWillMutate(); | ||||
|                                             } | ||||
|  | ||||
|                                             // move from source index ... | ||||
|                                             underlyingList.splice(sourceIndex, 1); | ||||
|                                             // ... to target index | ||||
|                                             underlyingList.splice(targetIndex, 0, item); | ||||
|  | ||||
|                                             // notify subscribers | ||||
|                                             if (sourceParent.valueHasMutated) { | ||||
|                                                 sourceParent.valueHasMutated(); | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                     else { | ||||
|                                         // drop new element from outside | ||||
|                                         targetParent.splice(targetIndex, 0, item); | ||||
|  | ||||
|                                         //rendering is handled by manipulating the observableArray; ignore dropped element | ||||
|                                         dataSet(el, ITEMKEY, null); | ||||
|                                         ui.item.remove(); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             //if using deferred updates plugin, force updates | ||||
|                             if (ko.processAllDeferredBindingUpdates) { | ||||
|                                 ko.processAllDeferredBindingUpdates(); | ||||
|                             } | ||||
|  | ||||
|                             //allow binding to accept a function to execute after moving the item | ||||
|                             if (sortable.afterMove) { | ||||
|                                 sortable.afterMove.call(this, arg, event, ui); | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         if (updateActual) { | ||||
|                             updateActual.apply(this, arguments); | ||||
|                         } | ||||
|                     }, | ||||
|                     connectWith: sortable.connectClass ? "." + sortable.connectClass : false | ||||
|                 })); | ||||
|  | ||||
|                 //handle enabling/disabling sorting | ||||
|                 if (sortable.isEnabled !== undefined) { | ||||
|                     ko.computed({ | ||||
|                         read: function() { | ||||
|                             $element.sortable(unwrap(sortable.isEnabled) ? "enable" : "disable"); | ||||
|                         }, | ||||
|                         disposeWhenNodeIsRemoved: element | ||||
|                     }); | ||||
|                 } | ||||
|             }, 0); | ||||
|  | ||||
|             //handle disposal | ||||
|             ko.utils.domNodeDisposal.addDisposeCallback(element, function() { | ||||
|                 //only call destroy if sortable has been created | ||||
|                 if ($element.data("ui-sortable") || $element.data("sortable")) { | ||||
|                     $element.sortable("destroy"); | ||||
|                 } | ||||
|  | ||||
|                 ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, false); | ||||
|  | ||||
|                 //do not create the sortable if the element has been removed from DOM | ||||
|                 clearTimeout(createTimeout); | ||||
|             }); | ||||
|  | ||||
|             return { 'controlsDescendantBindings': true }; | ||||
|         }, | ||||
|         update: function(element, valueAccessor, allBindingsAccessor, data, context) { | ||||
|             var templateOptions = prepareTemplateOptions(valueAccessor, "foreach"); | ||||
|  | ||||
|             //attach meta-data | ||||
|             dataSet(element, LISTKEY, templateOptions.foreach); | ||||
|  | ||||
|             //call template binding's update with correct options | ||||
|             ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context); | ||||
|         }, | ||||
|         connectClass: 'ko_container', | ||||
|         allowDrop: true, | ||||
|         afterMove: null, | ||||
|         beforeMove: null, | ||||
|         options: {} | ||||
|     }; | ||||
|  | ||||
|     //create a draggable that is appropriate for dropping into a sortable | ||||
|     ko.bindingHandlers.draggable = { | ||||
|         init: function(element, valueAccessor, allBindingsAccessor, data, context) { | ||||
|             var value = unwrap(valueAccessor()) || {}, | ||||
|                 options = value.options || {}, | ||||
|                 draggableOptions = ko.utils.extend({}, ko.bindingHandlers.draggable.options), | ||||
|                 templateOptions = prepareTemplateOptions(valueAccessor, "data"), | ||||
|                 connectClass = value.connectClass || ko.bindingHandlers.draggable.connectClass, | ||||
|                 isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.draggable.isEnabled; | ||||
|  | ||||
|             value = "data" in value ? value.data : value; | ||||
|  | ||||
|             //set meta-data | ||||
|             dataSet(element, DRAGKEY, value); | ||||
|  | ||||
|             //override global options with override options passed in | ||||
|             ko.utils.extend(draggableOptions, options); | ||||
|  | ||||
|             //setup connection to a sortable | ||||
|             draggableOptions.connectToSortable = connectClass ? "." + connectClass : false; | ||||
|  | ||||
|             //initialize draggable | ||||
|             $(element).draggable(draggableOptions); | ||||
|  | ||||
|             //handle enabling/disabling sorting | ||||
|             if (isEnabled !== undefined) { | ||||
|                 ko.computed({ | ||||
|                     read: function() { | ||||
|                         $(element).draggable(unwrap(isEnabled) ? "enable" : "disable"); | ||||
|                     }, | ||||
|                     disposeWhenNodeIsRemoved: element | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             //handle disposal | ||||
|             ko.utils.domNodeDisposal.addDisposeCallback(element, function() { | ||||
|                 $(element).draggable("destroy"); | ||||
|             }); | ||||
|  | ||||
|             return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context); | ||||
|         }, | ||||
|         update: function(element, valueAccessor, allBindingsAccessor, data, context) { | ||||
|             var templateOptions = prepareTemplateOptions(valueAccessor, "data"); | ||||
|  | ||||
|             return ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context); | ||||
|         }, | ||||
|         connectClass: ko.bindingHandlers.sortable.connectClass, | ||||
|         options: { | ||||
|             helper: "clone" | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Simple Droppable Implementation | ||||
|     // binding that updates (function or observable) | ||||
|     ko.bindingHandlers.droppable = { | ||||
|         init: function(element, valueAccessor, allBindingsAccessor, data, context) { | ||||
|             var value = unwrap(valueAccessor()) || {}, | ||||
|                 options = value.options || {}, | ||||
|                 droppableOptions = ko.utils.extend({}, ko.bindingHandlers.droppable.options), | ||||
|                 isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.droppable.isEnabled; | ||||
|  | ||||
|             //override global options with override options passed in | ||||
|             ko.utils.extend(droppableOptions, options); | ||||
|  | ||||
|             //get reference to drop method | ||||
|             value = "data" in value ? value.data : valueAccessor(); | ||||
|  | ||||
|             //set drop method | ||||
|             droppableOptions.drop = function(event, ui) { | ||||
|                 var droppedItem = dataGet(ui.draggable[0], DRAGKEY) || dataGet(ui.draggable[0], ITEMKEY); | ||||
|                 value(droppedItem); | ||||
|             }; | ||||
|  | ||||
|             //initialize droppable | ||||
|             $(element).droppable(droppableOptions); | ||||
|  | ||||
|             //handle enabling/disabling droppable | ||||
|             if (isEnabled !== undefined) { | ||||
|                 ko.computed({ | ||||
|                     read: function() { | ||||
|                         $(element).droppable(unwrap(isEnabled) ? "enable": "disable"); | ||||
|                     }, | ||||
|                     disposeWhenNodeIsRemoved: element | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             //handle disposal | ||||
|             ko.utils.domNodeDisposal.addDisposeCallback(element, function() { | ||||
|                 $(element).droppable("destroy"); | ||||
|             }); | ||||
|         }, | ||||
|         options: { | ||||
|             accept: "*" | ||||
|         } | ||||
|     }; | ||||
| }); | ||||
| @@ -1,31 +0,0 @@ | ||||
| <div id="bambu_printer_print_options" class="modal hide fade"> | ||||
| 	<div class="modal-header"> | ||||
| 		<h3>{{ _('Bambu Print Options') }}</h3> | ||||
| 	</div> | ||||
|     <div class="modal-body"> | ||||
|         <div class="row-fluid"> | ||||
|             <div class="span6"> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.timelapse"> {{ _('Enable timelapse') }}</label> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.bed_leveling"> {{ _('Enable bed leveling') }}</label> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.flow_cali"> {{ _('Enable flow calibration') }}</label> | ||||
|             </div> | ||||
|             <div class="span6"> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.vibration_cali"> {{ _('Enable vibration calibration') }}</label> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.layer_inspect"> {{ _('Enable first layer inspection') }}</label> | ||||
|                 <label class="checkbox"><input type="checkbox" data-bind="checked: settingsViewModel.settings.plugins.bambu_printer.use_ams"> {{ _('Use AMS') }}</label> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="row-fluid" data-bind="visible: settingsViewModel.settings.plugins.bambu_printer.use_ams"> | ||||
|             {{ _('Filament Assighnment') }}: {{ _('Click') }} <a href="#">{{ _('here') }}</a> {{ _('for usage details.') }} | ||||
|         </div> | ||||
|         <div class="row-fluid" data-bind="visible: settingsViewModel.settings.plugins.bambu_printer.use_ams, sortable: {data: ams_mapping, options: {cancel: '.unsortable'}}"> | ||||
|             <div class="btn" data-bind="attr: {title: name}, event: {dblclick: $root.toggle_spool_active}, css: {disabled: (index()<0)}"> | ||||
|                 <i class="fa fa-2x fa-dot-circle" data-bind="css: {'fas': !empty(), 'far': empty()}, style: {'color': ('#'+color())}"></i> | ||||
|             </div> | ||||
|         </div> | ||||
| 	</div> | ||||
| 	<div class="modal-footer"> | ||||
| 		<button class="btn btn-danger" data-bind="click: cancel_print_options">{{ _('Cancel') }}</button> | ||||
|         <button class="btn btn-primary" data-bind="click: accept_print_options">{{ _('Print') }}</button> | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -40,24 +40,15 @@ | ||||
| 	<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()"> | ||||
| 		<label class="control-label">{{ _('Email') }}</label> | ||||
| 		<div class="controls"> | ||||
| 			<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.email" title="{{ _('Registered email address') }}" autocomplete="off"></input> | ||||
| 			<input type="text" class="input-block-level" data-bind="value: settingsViewModel.settings.plugins.bambu_printer.email" title="{{ _('Registered email address') }}"></input> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt() && show_password()"> | ||||
| 	<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()"> | ||||
| 		<label class="control-label">{{ _('Password') }}</label> | ||||
| 		<div class="controls"> | ||||
|             <div class="input-block-level input-append" data-bind="css: {'input-append': !show_verification()}"> | ||||
| 			    <input id="bambu_cloud_password" type="password" class="input-text input-block-level" title="{{ _('Password to generate verification code') }}" autocomplete="new-password"></input> | ||||
|                 <span class="btn btn-primary add-on" data-bind="visible: !show_verification(), click: getAuthToken">{{ _('Login') }}</span> | ||||
|             </div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="control-group" data-bind="visible: show_verification()"> | ||||
| 		<label class="control-label">{{ _('Verify') }}</label> | ||||
| 		<div class="controls"> | ||||
|             <div class="input-block-level input-append"> | ||||
| 			    <input id="bambu_cloud_verify_code" type="password" class="input-text input-block-level" title="{{ _('Verification code to generate auth token') }}"></input> | ||||
|                 <span class="btn btn-primary add-on" data-bind="click: verifyCode">{{ _('Verify') }}</span> | ||||
| 			    <input id="bambu_cloud_password" type="password" class="input-text input-block-level" title="{{ _('Password to generate Auth Token') }}"></input> | ||||
|                 <span class="btn btn-primary add-on" data-bind="click: getAuthToken">{{ _('Login') }}</span> | ||||
|             </div> | ||||
| 		</div> | ||||
| 	</div> | ||||
|   | ||||
| @@ -1,15 +0,0 @@ | ||||
| <div class="row-fluid" data-bind="foreach: {data: settingsViewModel.settings.plugins.bambu_printer.ams_data, as: 'ams'}"> | ||||
|     <!-- ko if: $data --> | ||||
|     <div class="well" data-bind="foreach: tray"> | ||||
|         <div class="span3 text-center" data-bind="attr: {title: name}"> | ||||
|             <div class="row-fluid" data-bind="css: {'active': ($root.settingsViewModel.settings.plugins.bambu_printer.ams_current_tray() == (($parentContext.$index() * 4) + $index()))}"> | ||||
|                 <i class="fa fa-3x fa-dot-circle" data-bind="css: {'fas': !empty(), 'far': empty()}, style: {'color': ('#'+color())}"></i><br> | ||||
|                 <div class="text-center" data-bind="text: type"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <!-- /ko --> | ||||
| </div> | ||||
| <div class="row-fluid" data-bind="visible: job_info"> | ||||
|     <div class="span6">{{ _('Layer') }}:</div><div class="span6" data-bind="text: function(){return (job_info.current_layer() + ' of ' + job_info.total_layers);}"></div> | ||||
| </div> | ||||
| @@ -14,3 +14,4 @@ OctoPrint~=1.10.2 | ||||
| setuptools~=70.0.0 | ||||
| pyserial~=3.5 | ||||
| Flask~=2.2.5 | ||||
| paho-mqtt~=2.1.0 | ||||
|   | ||||
							
								
								
									
										12
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								setup.py
									
									
									
									
									
								
							| @@ -14,26 +14,26 @@ plugin_package = "octoprint_bambu_printer" | ||||
| plugin_name = "OctoPrint-BambuPrinter" | ||||
|  | ||||
| # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module | ||||
| plugin_version = "0.1.8rc12" | ||||
| plugin_version = "1.0.0" | ||||
|  | ||||
| # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin | ||||
| # module | ||||
| plugin_description = """Connects OctoPrint to BambuLabs printers.""" | ||||
|  | ||||
| # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module | ||||
| plugin_author = "jneilliii" | ||||
| plugin_author = "ManuelW" | ||||
|  | ||||
| # The plugin's author's mail address. | ||||
| plugin_author_email = "jneilliii+github@gmail.com" | ||||
| plugin_author_email = "manuelw@example.com" | ||||
|  | ||||
| # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module | ||||
| plugin_url = "https://github.com/jneilliii/OctoPrint-BambuPrinter" | ||||
| plugin_url = "https://gitlab.fire-devils.org/3D-Druck/OctoPrint-BambuPrinter" | ||||
|  | ||||
| # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module | ||||
| plugin_license = "AGPLv3" | ||||
|  | ||||
| # Any additional requirements besides OctoPrint should be listed here | ||||
| plugin_requires = ["paho-mqtt<2", "python-dateutil", "cloudscraper"] | ||||
| plugin_requires = ["paho-mqtt<2", "python-dateutil", "pybambu>=1.0.1"] | ||||
|  | ||||
| ### -------------------------------------------------------------------------------------------------------------------- | ||||
| ### More advanced options that you usually shouldn't have to touch follow after this point | ||||
| @@ -43,7 +43,7 @@ plugin_requires = ["paho-mqtt<2", "python-dateutil", "cloudscraper"] | ||||
| # already be installed automatically if they exist. Note that if you add something here you'll also need to update | ||||
| # MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your | ||||
| # files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598 | ||||
| plugin_additional_data = ["octoprint_bambu_printer/printer/pybambu/filaments.json"] | ||||
| plugin_additional_data = [] | ||||
|  | ||||
| # Any additional python packages you need to install with your plugin that are not contained in <plugin_package>.* | ||||
| plugin_additional_packages = [] | ||||
|   | ||||
| @@ -7,8 +7,8 @@ from typing import Any | ||||
| from unittest.mock import MagicMock, patch | ||||
|  | ||||
| from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | ||||
| import octoprint_bambu_printer.printer.pybambu | ||||
| import octoprint_bambu_printer.printer.pybambu.commands | ||||
| import pybambu | ||||
| import pybambu.commands | ||||
| from octoprint_bambu_printer.printer.bambu_virtual_printer import BambuVirtualPrinter | ||||
| from octoprint_bambu_printer.printer.file_system.file_info import FileInfo | ||||
| from octoprint_bambu_printer.printer.file_system.ftps_client import IoTFTPSClient | ||||
|   | ||||
		Reference in New Issue
	
	Block a user