Compare commits
	
		
			2 Commits
		
	
	
		
			0.1.8rc12
			...
			698f8f4151
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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/templates * | ||||||
| recursive-include octoprint_bambu_printer/translations * | recursive-include octoprint_bambu_printer/translations * | ||||||
| recursive-include octoprint_bambu_printer/static * | recursive-include octoprint_bambu_printer/static * | ||||||
| include octoprint_bambu_printer/printer/pybambu/filaments.json |  | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| from __future__ import absolute_import, annotations | from __future__ import absolute_import, annotations | ||||||
|  |  | ||||||
| import json |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import threading | import threading | ||||||
| from time import perf_counter | from time import perf_counter | ||||||
| @@ -8,8 +6,6 @@ from contextlib import contextmanager | |||||||
| import flask | import flask | ||||||
| import logging.handlers | import logging.handlers | ||||||
| from urllib.parse import quote as urlquote | from urllib.parse import quote as urlquote | ||||||
| import os |  | ||||||
| import zipfile |  | ||||||
|  |  | ||||||
| import octoprint.printer | import octoprint.printer | ||||||
| import octoprint.server | import octoprint.server | ||||||
| @@ -26,7 +22,7 @@ from octoprint.access.permissions import Permissions | |||||||
| from octoprint.logging.handlers import CleaningTimedRotatingFileHandler | from octoprint.logging.handlers import CleaningTimedRotatingFileHandler | ||||||
|  |  | ||||||
| from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | 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 ( | from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( | ||||||
|     RemoteSDCardFileList, |     RemoteSDCardFileList, | ||||||
| @@ -61,7 +57,6 @@ class BambuPrintPlugin( | |||||||
|     _plugin_manager: octoprint.plugin.PluginManager |     _plugin_manager: octoprint.plugin.PluginManager | ||||||
|     _bambu_file_system: RemoteSDCardFileList |     _bambu_file_system: RemoteSDCardFileList | ||||||
|     _timelapse_files_view: CachedFileView |     _timelapse_files_view: CachedFileView | ||||||
|     _bambu_cloud: None |  | ||||||
|  |  | ||||||
|     def on_settings_initialized(self): |     def on_settings_initialized(self): | ||||||
|         self._bambu_file_system = RemoteSDCardFileList(self._settings) |         self._bambu_file_system = RemoteSDCardFileList(self._settings) | ||||||
| @@ -72,9 +67,7 @@ class BambuPrintPlugin( | |||||||
|             self._timelapse_files_view.with_filter("timelapse/", ".avi") |             self._timelapse_files_view.with_filter("timelapse/", ".avi") | ||||||
|  |  | ||||||
|     def get_assets(self): |     def get_assets(self): | ||||||
|         return {"js": ["js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/bambu_printer.js"], |         return {"js": ["js/bambu_printer.js"]} | ||||||
|                 "css": ["css/bambu_printer.css"] |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|     def get_template_configs(self): |     def get_template_configs(self): | ||||||
|         return [ |         return [ | ||||||
| @@ -84,7 +77,7 @@ class BambuPrintPlugin( | |||||||
|                 "custom_bindings": True, |                 "custom_bindings": True, | ||||||
|                 "template": "bambu_timelapse.jinja2", |                 "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): |     def get_settings_defaults(self): | ||||||
|         return { |         return { | ||||||
| @@ -92,7 +85,7 @@ class BambuPrintPlugin( | |||||||
|             "serial": "", |             "serial": "", | ||||||
|             "host": "", |             "host": "", | ||||||
|             "access_code": "", |             "access_code": "", | ||||||
|             "username": "bblp", |             "username": "octobambu", | ||||||
|             "timelapse": False, |             "timelapse": False, | ||||||
|             "bed_leveling": True, |             "bed_leveling": True, | ||||||
|             "flow_cali": False, |             "flow_cali": False, | ||||||
| @@ -104,22 +97,13 @@ class BambuPrintPlugin( | |||||||
|             "email": "", |             "email": "", | ||||||
|             "auth_token": "", |             "auth_token": "", | ||||||
|             "always_use_default_options": False, |             "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): |     def is_api_adminonly(self): | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     def get_api_commands(self): |     def get_api_commands(self): | ||||||
|         return {"register": ["email", "password", "region", "auth_token"], |         return {"register": ["email", "password", "region", "auth_token"]} | ||||||
|                 "verify": ["auth_type", "password"]} |  | ||||||
|  |  | ||||||
|     def on_api_command(self, command, data): |     def on_api_command(self, command, data): | ||||||
|         if command == "register": |         if command == "register": | ||||||
| @@ -130,57 +114,20 @@ class BambuPrintPlugin( | |||||||
|                 and "auth_token" in data |                 and "auth_token" in data | ||||||
|             ): |             ): | ||||||
|                 self._logger.info(f"Registering user {data['email']}") |                 self._logger.info(f"Registering user {data['email']}") | ||||||
|                 self._bambu_cloud = BambuCloud( |                 bambu_cloud = BambuCloud( | ||||||
|                     data["region"], data["email"], data["password"], data["auth_token"] |                     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( |                 return flask.jsonify( | ||||||
|                     { |                     { | ||||||
|                         "auth_response": auth_response, |                         "auth_token": bambu_cloud.auth_token, | ||||||
|  |                         "username": bambu_cloud.username, | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|         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" |  | ||||||
|                         } |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|     def on_event(self, event, payload): |     def on_event(self, event, payload): | ||||||
|         if event == Events.TRANSFER_DONE: |         if event == Events.TRANSFER_DONE: | ||||||
|             self._printer.commands("M20 L T", force=True) |             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): |     def support_3mf_files(self): | ||||||
|         return {"machinecode": {"3mf": ["3mf"]}} |         return {"machinecode": {"3mf": ["3mf"]}} | ||||||
| @@ -339,10 +286,10 @@ class BambuPrintPlugin( | |||||||
|     def get_update_information(self): |     def get_update_information(self): | ||||||
|         return { |         return { | ||||||
|             "bambu_printer": { |             "bambu_printer": { | ||||||
|                 "displayName": "Bambu Printer", |                 "displayName": "Manus Bambu Printer", | ||||||
|                 "displayVersion": self._plugin_version, |                 "displayVersion": self._plugin_version, | ||||||
|                 "type": "github_release", |                 "type": "github_release", | ||||||
|                 "user": "jneilliii", |                 "user": "ManuelW", | ||||||
|                 "repo": "OctoPrint-BambuPrinter", |                 "repo": "OctoPrint-BambuPrinter", | ||||||
|                 "current": self._plugin_version, |                 "current": self._plugin_version, | ||||||
|                 "stable_branch": { |                 "stable_branch": { | ||||||
| @@ -357,6 +304,6 @@ class BambuPrintPlugin( | |||||||
|                         "comittish": ["rc", "master"], |                         "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", | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| import collections | import collections | ||||||
| from dataclasses import dataclass, field, asdict | from dataclasses import dataclass, field | ||||||
| import math | import math | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import queue | import queue | ||||||
| @@ -11,7 +11,7 @@ import time | |||||||
| from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | ||||||
| from octoprint_bambu_printer.printer.file_system.file_info import FileInfo | from octoprint_bambu_printer.printer.file_system.file_info import FileInfo | ||||||
| from octoprint_bambu_printer.printer.print_job import PrintJob | from octoprint_bambu_printer.printer.print_job import PrintJob | ||||||
| from octoprint_bambu_printer.printer.pybambu import BambuClient, commands | from pybambu import BambuClient, commands | ||||||
| import logging | import logging | ||||||
| import logging.handlers | import logging.handlers | ||||||
|  |  | ||||||
| @@ -43,7 +43,6 @@ class BambuPrinterTelemetry: | |||||||
|     lastTempAt: float = time.monotonic() |     lastTempAt: float = time.monotonic() | ||||||
|     firmwareName: str = "Bambu" |     firmwareName: str = "Bambu" | ||||||
|     extruderCount: int = 1 |     extruderCount: int = 1 | ||||||
|     ams_current_tray: int = 255 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # noinspection PyBroadException | # noinspection PyBroadException | ||||||
| @@ -65,7 +64,6 @@ class BambuVirtualPrinter: | |||||||
|         self._data_folder = data_folder |         self._data_folder = data_folder | ||||||
|         self._last_hms_errors = None |         self._last_hms_errors = None | ||||||
|         self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") |         self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") | ||||||
|         self.ams_data = self._settings.get(["ams_data"]) |  | ||||||
|  |  | ||||||
|         self._state_idle = IdleState(self) |         self._state_idle = IdleState(self) | ||||||
|         self._state_printing = PrintingState(self) |         self._state_printing = PrintingState(self) | ||||||
| @@ -180,14 +178,6 @@ class BambuVirtualPrinter: | |||||||
|         device_data = self.bambu_client.get_device() |         device_data = self.bambu_client.get_device() | ||||||
|         print_job_state = device_data.print_job.gcode_state |         print_job_state = device_data.print_job.gcode_state | ||||||
|         temperatures = device_data.temperature |         temperatures = device_data.temperature | ||||||
|         # strip out extra data to avoid unneeded settings updates |  | ||||||
|         ams_data = [{"tray": asdict(x).pop("tray", None)} for x in device_data.ams.data if x is not None] |  | ||||||
|  |  | ||||||
|         if self.ams_data != ams_data: |  | ||||||
|             self._log.debug(f"Recieveid AMS Update: {ams_data}") |  | ||||||
|             self.ams_data = ams_data |  | ||||||
|             self._settings.set(["ams_data"], ams_data) |  | ||||||
|             self._settings.save(trigger_event=True) |  | ||||||
|  |  | ||||||
|         self.lastTempAt = time.monotonic() |         self.lastTempAt = time.monotonic() | ||||||
|         self._telemetry.temp[0] = temperatures.nozzle_temp |         self._telemetry.temp[0] = temperatures.nozzle_temp | ||||||
| @@ -195,12 +185,6 @@ class BambuVirtualPrinter: | |||||||
|         self._telemetry.bedTemp = temperatures.bed_temp |         self._telemetry.bedTemp = temperatures.bed_temp | ||||||
|         self._telemetry.bedTargetTemp = temperatures.target_bed_temp |         self._telemetry.bedTargetTemp = temperatures.target_bed_temp | ||||||
|         self._telemetry.chamberTemp = temperatures.chamber_temp |         self._telemetry.chamberTemp = temperatures.chamber_temp | ||||||
|         if device_data.push_all_data and "ams" in device_data.push_all_data: |  | ||||||
|             self._telemetry.ams_current_tray = device_data.push_all_data["ams"]["tray_now"] or 255 |  | ||||||
|  |  | ||||||
|         if self._telemetry.ams_current_tray != self._settings.get_int(["ams_current_tray"]): |  | ||||||
|             self._settings.set_int(["ams_current_tray"], self._telemetry.ams_current_tray) |  | ||||||
|             self._settings.save(trigger_event=True) |  | ||||||
|  |  | ||||||
|         self._log.debug(f"Received printer state update: {print_job_state}") |         self._log.debug(f"Received printer state update: {print_job_state}") | ||||||
|         if ( |         if ( | ||||||
| @@ -230,8 +214,6 @@ class BambuVirtualPrinter: | |||||||
|  |  | ||||||
|     def on_disconnect(self, on_disconnect): |     def on_disconnect(self, on_disconnect): | ||||||
|         self._log.debug(f"on disconnect called") |         self._log.debug(f"on disconnect called") | ||||||
|         self.stop_continuous_status_report() |  | ||||||
|         self.stop_continuous_temp_report() |  | ||||||
|         return on_disconnect |         return on_disconnect | ||||||
|  |  | ||||||
|     def on_connect(self, on_connect): |     def on_connect(self, on_connect): | ||||||
| @@ -259,20 +241,15 @@ class BambuVirtualPrinter: | |||||||
|             f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}" |             f"connecting via local mqtt: {self._settings.get_boolean(['local_mqtt'])}" | ||||||
|         ) |         ) | ||||||
|         bambu_client = BambuClient( |         bambu_client = BambuClient( | ||||||
|             {"device_type": self._settings.get(["device_type"]), |             device_type=self._settings.get(["device_type"]), | ||||||
|             "serial": self._settings.get(["serial"]), |             serial=self._settings.get(["serial"]), | ||||||
|             "host": self._settings.get(["host"]), |             host=self._settings.get(["host"]), | ||||||
|             "username": ( |             username=("bambuocto"), | ||||||
|                 "bblp" |             access_code=self._settings.get(["access_code"]), | ||||||
|                 if self._settings.get_boolean(["local_mqtt"]) |             local_mqtt=self._settings.get_boolean(["local_mqtt"]), | ||||||
|                 else self._settings.get(["username"]) |             region=self._settings.get(["region"]), | ||||||
|             ), |             email=self._settings.get(["email"]), | ||||||
|             "access_code": self._settings.get(["access_code"]), |             auth_token=self._settings.get(["auth_token"]), | ||||||
|             "local_mqtt": self._settings.get_boolean(["local_mqtt"]), |  | ||||||
|             "region": self._settings.get(["region"]), |  | ||||||
|             "email": self._settings.get(["email"]), |  | ||||||
|             "auth_token": self._settings.get(["auth_token"]) if self._settings.get_boolean(["local_mqtt"]) is False else "", |  | ||||||
|              } |  | ||||||
|         ) |         ) | ||||||
|         bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect) |         bambu_client.on_disconnect = self.on_disconnect(bambu_client.on_disconnect) | ||||||
|         bambu_client.on_connect = self.on_connect(bambu_client.on_connect) |         bambu_client.on_connect = self.on_connect(bambu_client.on_connect) | ||||||
| @@ -331,21 +308,21 @@ class BambuVirtualPrinter: | |||||||
|         self._selected_project_file = None |         self._selected_project_file = None | ||||||
|  |  | ||||||
|     def select_project_file(self, file_path: str) -> bool: |     def select_project_file(self, file_path: str) -> bool: | ||||||
|         file_info = self._project_files_view.get_file_by_name(file_path) |         self._log.debug(f"Select project file: {file_path}") | ||||||
|  |         file_info = self._project_files_view.get_file_by_stem( | ||||||
|  |             file_path, [".gcode", ".3mf"] | ||||||
|  |         ) | ||||||
|         if ( |         if ( | ||||||
|             self._selected_project_file is not None |             self._selected_project_file is not None | ||||||
|             and file_info is not None |             and file_info is not None | ||||||
|             and self._selected_project_file.path == file_info.path |             and self._selected_project_file.path == file_info.path | ||||||
|         ): |         ): | ||||||
|             self._log.debug(f"File already selected: {file_path}") |  | ||||||
|             return True |             return True | ||||||
|  |  | ||||||
|         if file_info is None: |         if file_info is None: | ||||||
|             self._log.error(f"Cannot select non-existent file: {file_path}") |             self._log.error(f"Cannot select not existing file: {file_path}") | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         self._log.debug(f"Select project file: {file_path}") |  | ||||||
|  |  | ||||||
|         self._selected_project_file = file_info |         self._selected_project_file = file_info | ||||||
|         self._send_file_selected_message() |         self._send_file_selected_message() | ||||||
|         return True |         return True | ||||||
| @@ -353,9 +330,8 @@ class BambuVirtualPrinter: | |||||||
|     ##~~ command implementations |     ##~~ command implementations | ||||||
|  |  | ||||||
|     @gcode_executor.register_no_data("M21") |     @gcode_executor.register_no_data("M21") | ||||||
|     def _sd_status(self) -> bool: |     def _sd_status(self) -> None: | ||||||
|         self.sendIO("SD card ok") |         self.sendIO("SD card ok") | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     @gcode_executor.register("M23") |     @gcode_executor.register("M23") | ||||||
|     def _select_sd_file(self, data: str) -> bool: |     def _select_sd_file(self, data: str) -> bool: | ||||||
| @@ -456,9 +432,6 @@ class BambuVirtualPrinter: | |||||||
|     # noinspection PyUnusedLocal |     # noinspection PyUnusedLocal | ||||||
|     @gcode_executor.register_no_data("M115") |     @gcode_executor.register_no_data("M115") | ||||||
|     def _report_firmware_info(self) -> bool: |     def _report_firmware_info(self) -> bool: | ||||||
|         # wait for connection to be established before sending back firmware info |  | ||||||
|         while self.bambu_client.connected is False: |  | ||||||
|             time.sleep(1) |  | ||||||
|         self.sendIO("Bambu Printer Integration") |         self.sendIO("Bambu Printer Integration") | ||||||
|         self.sendIO("Cap:AUTOREPORT_SD_STATUS:1") |         self.sendIO("Cap:AUTOREPORT_SD_STATUS:1") | ||||||
|         self.sendIO("Cap:AUTOREPORT_TEMP:1") |         self.sendIO("Cap:AUTOREPORT_TEMP:1") | ||||||
| @@ -671,7 +644,7 @@ class BambuVirtualPrinter: | |||||||
|         self._state_change_queue.join() |         self._state_change_queue.join() | ||||||
|  |  | ||||||
|     def _printer_worker(self): |     def _printer_worker(self): | ||||||
|         # self._create_client_connection_async() |         self._create_client_connection_async() | ||||||
|         self.sendIO("Printer connection complete") |         self.sendIO("Printer connection complete") | ||||||
|         while self._running: |         while self._running: | ||||||
|             try: |             try: | ||||||
|   | |||||||
| @@ -35,8 +35,8 @@ class CachedFileView: | |||||||
|         result: list[FileInfo] = [] |         result: list[FileInfo] = [] | ||||||
|  |  | ||||||
|         with self.file_system.get_ftps_client() as ftp: |         with self.file_system.get_ftps_client() as ftp: | ||||||
|             for key in self.folder_view.keys(): |             for filter in self.folder_view.keys(): | ||||||
|                 result.extend(self.file_system.list_files(*key, ftp, existing_files)) |                 result.extend(self.file_system.list_files(*filter, ftp, existing_files)) | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
|     def update(self): |     def update(self): | ||||||
| @@ -56,9 +56,6 @@ class CachedFileView: | |||||||
|     def get_all_cached_info(self): |     def get_all_cached_info(self): | ||||||
|         return list(self._file_data_cache.values()) |         return list(self._file_data_cache.values()) | ||||||
|  |  | ||||||
|     def get_keys_as_list(self): |  | ||||||
|         return list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()) |  | ||||||
|  |  | ||||||
|     def get_file_data(self, file_path: str | Path) -> FileInfo | None: |     def get_file_data(self, file_path: str | Path) -> FileInfo | None: | ||||||
|         file_data = self.get_file_data_cached(file_path) |         file_data = self.get_file_data_cached(file_path) | ||||||
|         if file_data is None: |         if file_data is None: | ||||||
| @@ -76,23 +73,22 @@ class CachedFileView: | |||||||
|             file_path = self._file_alias_cache.get(file_path, file_path) |             file_path = self._file_alias_cache.get(file_path, file_path) | ||||||
|         return self._file_data_cache.get(file_path, None) |         return self._file_data_cache.get(file_path, None) | ||||||
|  |  | ||||||
|     def get_file_by_name(self, file_name: str): |     def get_file_by_stem(self, file_stem: str, allowed_suffixes: list[str]): | ||||||
|         if file_name == "": |         if file_stem == "": | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|         file_list = self.get_keys_as_list() |         file_stem = Path(file_stem).with_suffix("").stem | ||||||
|         if not file_name in file_list: |         file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) | ||||||
|             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: |         if file_data is None: | ||||||
|             self.update() |             self.update() | ||||||
|             return self.get_file_by_name(file_name) |             file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) | ||||||
|         return file_data |         return file_data | ||||||
|  |  | ||||||
|  |     def _get_file_by_stem_cached(self, file_stem: str, allowed_suffixes: list[str]): | ||||||
|  |         for file_path_str in list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()): | ||||||
|  |             file_path = Path(file_path_str) | ||||||
|  |             if file_stem == file_path.with_suffix("").stem and all( | ||||||
|  |                 suffix in allowed_suffixes for suffix in file_path.suffixes | ||||||
|  |             ): | ||||||
|  |                 return self.get_file_data_cached(file_path) | ||||||
|  |         return None | ||||||
|   | |||||||
| @@ -10,9 +10,6 @@ from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import | |||||||
| class PrintJob: | class PrintJob: | ||||||
|     file_info: FileInfo |     file_info: FileInfo | ||||||
|     progress: int |     progress: int | ||||||
|     remaining_time: int |  | ||||||
|     current_layer: int |  | ||||||
|     total_layers: int |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def file_position(self): |     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 = ( |         filesystem_root = ( | ||||||
|             "file:///mnt/sdcard/" |             "file:///mnt/sdcard/" | ||||||
|             if self._printer._settings.get(["device_type"]) in ["X1", "X1C"] |             if self._printer._settings.get(["device_type"]) in ["X1", "X1C"] | ||||||
|             else "file:///sdcard/" |             else "file:///" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         print_command = { |         print_command = { | ||||||
| @@ -49,7 +49,7 @@ class IdleState(APrinterState): | |||||||
|                 ), |                 ), | ||||||
|                 "layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]), |                 "layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]), | ||||||
|                 "use_ams": self._printer._settings.get_boolean(["use_ams"]), |                 "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 threading | ||||||
|  |  | ||||||
| import octoprint_bambu_printer.printer.pybambu.commands | import pybambu.commands | ||||||
| from octoprint.util import RepeatedTimer | from octoprint.util import RepeatedTimer | ||||||
|  |  | ||||||
| from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | ||||||
| @@ -37,14 +37,14 @@ class PausedState(APrinterState): | |||||||
|  |  | ||||||
|     def start_new_print(self): |     def start_new_print(self): | ||||||
|         if self._printer.bambu_client.connected: |         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") |                 self._log.info("print resumed") | ||||||
|             else: |             else: | ||||||
|                 self._log.info("print resume failed") |                 self._log.info("print resume failed") | ||||||
|  |  | ||||||
|     def cancel_print(self): |     def cancel_print(self): | ||||||
|         if self._printer.bambu_client.connected: |         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._log.info("print cancelled") | ||||||
|                 self._printer.finalize_print_job() |                 self._printer.finalize_print_job() | ||||||
|             else: |             else: | ||||||
|   | |||||||
| @@ -10,9 +10,9 @@ if TYPE_CHECKING: | |||||||
|  |  | ||||||
| import threading | import threading | ||||||
|  |  | ||||||
| import octoprint_bambu_printer.printer.pybambu | import pybambu | ||||||
| import octoprint_bambu_printer.printer.pybambu.models | import pybambu.models | ||||||
| import octoprint_bambu_printer.printer.pybambu.commands | import pybambu.commands | ||||||
|  |  | ||||||
| from octoprint_bambu_printer.printer.print_job import PrintJob | from octoprint_bambu_printer.printer.print_job import PrintJob | ||||||
| from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState | ||||||
| @@ -22,7 +22,7 @@ class PrintingState(APrinterState): | |||||||
|  |  | ||||||
|     def __init__(self, printer: BambuVirtualPrinter) -> None: |     def __init__(self, printer: BambuVirtualPrinter) -> None: | ||||||
|         super().__init__(printer) |         super().__init__(printer) | ||||||
|         self._printer.current_print_job = None |         self._current_print_job = None | ||||||
|         self._is_printing = False |         self._is_printing = False | ||||||
|         self._sd_printing_thread = None |         self._sd_printing_thread = None | ||||||
|  |  | ||||||
| @@ -40,15 +40,12 @@ class PrintingState(APrinterState): | |||||||
|         self._printer.current_print_job = None |         self._printer.current_print_job = None | ||||||
|  |  | ||||||
|     def _start_worker_thread(self): |     def _start_worker_thread(self): | ||||||
|         self._is_printing = True |  | ||||||
|         if self._sd_printing_thread is None: |         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 = threading.Thread(target=self._printing_worker) | ||||||
|             self._sd_printing_thread.start() |             self._sd_printing_thread.start() | ||||||
|         else: |  | ||||||
|             self._sd_printing_thread.join() |  | ||||||
|  |  | ||||||
|     def _printing_worker(self): |     def _printing_worker(self): | ||||||
|         self._log.debug(f"_printing_worker before while loop: {self._printer.current_print_job}") |  | ||||||
|         while ( |         while ( | ||||||
|             self._is_printing |             self._is_printing | ||||||
|             and self._printer.current_print_job is not None |             and self._printer.current_print_job is not None | ||||||
| @@ -58,7 +55,6 @@ class PrintingState(APrinterState): | |||||||
|             self._printer.report_print_job_status() |             self._printer.report_print_job_status() | ||||||
|             time.sleep(3) |             time.sleep(3) | ||||||
|  |  | ||||||
|         self._log.debug(f"_printing_worker after while loop: {self._printer.current_print_job}") |  | ||||||
|         self.update_print_job_info() |         self.update_print_job_info() | ||||||
|         if ( |         if ( | ||||||
|             self._printer.current_print_job is not None |             self._printer.current_print_job is not None | ||||||
| @@ -68,34 +64,30 @@ class PrintingState(APrinterState): | |||||||
|  |  | ||||||
|     def update_print_job_info(self): |     def update_print_job_info(self): | ||||||
|         print_job_info = self._printer.bambu_client.get_device().print_job |         print_job_info = self._printer.bambu_client.get_device().print_job | ||||||
|         subtask_name: str = print_job_info.subtask_name |         task_name: str = print_job_info.subtask_name | ||||||
|         gcode_file: str = print_job_info.gcode_file |         project_file_info = self._printer.project_files.get_file_by_stem( | ||||||
|  |             task_name, [".gcode", ".3mf"] | ||||||
|         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) |  | ||||||
|         if project_file_info is None: |         if project_file_info is None: | ||||||
|             self._log.debug(f"No 3mf file found for {print_job_info}") |             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) |             self._printer.change_state(self._printer._state_idle) | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         progress = print_job_info.print_percentage |         progress = print_job_info.print_percentage | ||||||
|         if print_job_info.gcode_state == "PREPARE" and progress == 100: |         self._printer.current_print_job = PrintJob(project_file_info, progress) | ||||||
|             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.select_project_file(project_file_info.path.as_posix()) |         self._printer.select_project_file(project_file_info.path.as_posix()) | ||||||
|  |  | ||||||
|     def pause_print(self): |     def pause_print(self): | ||||||
|         if self._printer.bambu_client.connected: |         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") |                 self._log.info("print paused") | ||||||
|             else: |             else: | ||||||
|                 self._log.info("print pause failed") |                 self._log.info("print pause failed") | ||||||
|  |  | ||||||
|     def cancel_print(self): |     def cancel_print(self): | ||||||
|         if self._printer.bambu_client.connected: |         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._log.info("print cancelled") | ||||||
|                 self._printer.finalize_print_job() |                 self._printer.finalize_print_job() | ||||||
|             else: |             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.accessViewModel = parameters[3]; | ||||||
|         self.timelapseViewModel = parameters[4]; |         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.getAuthToken = function (data) { | ||||||
|             self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); |             self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); | ||||||
|             self.auth_type(""); |  | ||||||
|             OctoPrint.simpleApiCommand("bambu_printer", "register", { |             OctoPrint.simpleApiCommand("bambu_printer", "register", { | ||||||
|                 "email": self.settingsViewModel.settings.plugins.bambu_printer.email(), |                 "email": self.settingsViewModel.settings.plugins.bambu_printer.email(), | ||||||
|                 "password": $("#bambu_cloud_password").val(), |                 "password": $("#bambu_cloud_password").val(), | ||||||
|                 "region": self.settingsViewModel.settings.plugins.bambu_printer.region(), |                 "region": self.settingsViewModel.settings.plugins.bambu_printer.region(), | ||||||
|                 "auth_token": self.settingsViewModel.settings.plugins.bambu_printer.auth_token() |                 "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) { |                 .done(function (response) { | ||||||
|                     console.log(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.auth_token(response.auth_token); |                     self.settingsViewModel.settings.plugins.bambu_printer.username(response.username); | ||||||
|                         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) { |             if (data.files !== undefined) { | ||||||
|  |                 console.log(data.files); | ||||||
|                 self.listHelper.updateItems(data.files); |                 self.listHelper.updateItems(data.files); | ||||||
|                 self.listHelper.resetPage(); |                 self.listHelper.resetPage(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (data.job_info !== undefined) { |  | ||||||
|                 self.job_info(data.job_info); |  | ||||||
|             } |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         self.onBeforeBinding = function () { |         self.onBeforeBinding = function () { | ||||||
|             $('#bambu_timelapse').appendTo("#timelapse"); |             $('#bambu_timelapse').appendTo("#timelapse"); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         self.onAfterBinding = function () { |  | ||||||
|             console.log(self.ams_mapping_computed()); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         self.showTimelapseThumbnail = function(data) { |         self.showTimelapseThumbnail = function(data) { | ||||||
|             $("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail); |             $("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail); | ||||||
|             $("#bambu_printer_timelapse_preview").modal('show'); |             $("#bambu_printer_timelapse_preview").modal('show'); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         self.onBeforePrintStart = function(start_print_command, data) { |         /*$('#files div.upload-buttons > span.fileinput-button:first, #files div.folder-button').remove(); | ||||||
|             self.ams_mapping(self.ams_mapping_computed()); |         $('#files div.upload-buttons > span.fileinput-button:first').removeClass('span6').addClass('input-block-level'); | ||||||
|             self.start_print_command = start_print_command; |  | ||||||
|             self.use_ams = self.settingsViewModel.settings.plugins.bambu_printer.use_ams(); |         self.onBeforePrintStart = function(start_print_command) { | ||||||
|             // prevent starting locally stored files, once data is added to core OctoPrint this |             let confirmation_html = '' + | ||||||
|             // could be adjusted to include additional processing like get sliced file's |                 '            <div class="row-fluid form-vertical">\n' + | ||||||
|             // spool assignments and colors from plate_#.json inside 3mf file. |                 '                <div class="control-group">\n' + | ||||||
|             if(data && data.origin !== "sdcard") { |                 '                    <label class="control-label">' + gettext("Plate Number") + '</label>\n' + | ||||||
|                 return false; |                 '                    <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) { |             showConfirmationDialog({ | ||||||
|             if(data.index() >= 0){ |                 title: "Bambu Print Options", | ||||||
|                 data.original_index = ko.observable(data.index()); |                 html: confirmation_html, | ||||||
|                 data.index(-1); |                 cancel: gettext("Cancel"), | ||||||
|             } else { |                 proceed: [gettext("Print"), gettext("Always")], | ||||||
|                 data.index(data.original_index()); |                 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.cancel_print_options = function() { |                         self.settingsViewModel.settings.plugins.bambu_printer.flow_cali($('#bambu_printer_flow_cali').is(':checked')); | ||||||
|             self.settingsViewModel.settings.plugins.bambu_printer.use_ams(self.use_ams); |                         self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali($('#bambu_printer_vibration_cali').is(':checked')); | ||||||
|             $("#bambu_printer_print_options").modal('hide'); |                         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.accept_print_options = function() { |                         self.settingsViewModel.saveData(); | ||||||
|             console.log("starting print!!!!"); |                     } | ||||||
|             console.log(self.ams_mapping()); |                     // replace this with our own print command API call? | ||||||
|             $("#bambu_printer_print_options").modal('hide'); |                     start_print_command(); | ||||||
|             var flattened_ams_mapping = ko.utils.arrayMap(self.ams_mapping(), function(item) { |                 }, | ||||||
|                 return item.index(); |                 nofade: true | ||||||
|             }); |             }); | ||||||
|             self.settingsViewModel.settings.plugins.bambu_printer.ams_mapping(flattened_ams_mapping); |             return false; | ||||||
|             self.settingsViewModel.saveData(undefined, self.start_print_command); |         };*/ | ||||||
|             // self.settingsViewModel.saveData(); |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     OCTOPRINT_VIEWMODELS.push({ |     OCTOPRINT_VIEWMODELS.push({ | ||||||
|         construct: Bambu_printerViewModel, |         construct: Bambu_printerViewModel, | ||||||
|  |         // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... | ||||||
|         dependencies: ["settingsViewModel", "filesViewModel", "loginStateViewModel", "accessViewModel", "timelapseViewModel"], |         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()"> | 	<div class="control-group" data-bind="visible: !settingsViewModel.settings.plugins.bambu_printer.local_mqtt()"> | ||||||
| 		<label class="control-label">{{ _('Email') }}</label> | 		<label class="control-label">{{ _('Email') }}</label> | ||||||
| 		<div class="controls"> | 		<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> | 	</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> | 		<label class="control-label">{{ _('Password') }}</label> | ||||||
| 		<div class="controls"> | 		<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"> |             <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> | 			    <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: verifyCode">{{ _('Verify') }}</span> |                 <span class="btn btn-primary add-on" data-bind="click: getAuthToken">{{ _('Login') }}</span> | ||||||
|             </div> |             </div> | ||||||
| 		</div> | 		</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> |  | ||||||
							
								
								
									
										12
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								setup.py
									
									
									
									
									
								
							| @@ -14,26 +14,26 @@ plugin_package = "octoprint_bambu_printer" | |||||||
| plugin_name = "OctoPrint-BambuPrinter" | plugin_name = "OctoPrint-BambuPrinter" | ||||||
|  |  | ||||||
| # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module | # 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 | # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin | ||||||
| # module | # module | ||||||
| plugin_description = """Connects OctoPrint to BambuLabs printers.""" | 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 | # 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. | # 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 | # 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 | # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module | ||||||
| plugin_license = "AGPLv3" | plugin_license = "AGPLv3" | ||||||
|  |  | ||||||
| # Any additional requirements besides OctoPrint should be listed here | # 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 | ### 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 | # 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 | # 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 | # 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>.* | # Any additional python packages you need to install with your plugin that are not contained in <plugin_package>.* | ||||||
| plugin_additional_packages = [] | plugin_additional_packages = [] | ||||||
|   | |||||||
| @@ -7,8 +7,8 @@ from typing import Any | |||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView | ||||||
| import octoprint_bambu_printer.printer.pybambu | import pybambu | ||||||
| import octoprint_bambu_printer.printer.pybambu.commands | import pybambu.commands | ||||||
| from octoprint_bambu_printer.printer.bambu_virtual_printer import BambuVirtualPrinter | 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.file_info import FileInfo | ||||||
| from octoprint_bambu_printer.printer.file_system.ftps_client import IoTFTPSClient | from octoprint_bambu_printer.printer.file_system.ftps_client import IoTFTPSClient | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user