Compare commits
	
		
			2 Commits
		
	
	
		
			0.1.8rc4
			...
			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
 | 
				
			||||||
@@ -24,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,
 | 
				
			||||||
@@ -69,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 [
 | 
				
			||||||
@@ -81,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 {
 | 
				
			||||||
@@ -89,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,
 | 
				
			||||||
@@ -101,9 +97,6 @@ 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 is_api_adminonly(self):
 | 
					    def is_api_adminonly(self):
 | 
				
			||||||
@@ -293,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": {
 | 
				
			||||||
@@ -311,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)
 | 
				
			||||||
@@ -170,22 +168,6 @@ class BambuVirtualPrinter:
 | 
				
			|||||||
    def change_state(self, new_state: APrinterState):
 | 
					    def change_state(self, new_state: APrinterState):
 | 
				
			||||||
        self._state_change_queue.put(new_state)
 | 
					        self._state_change_queue.put(new_state)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _convert2serialize(self, obj):
 | 
					 | 
				
			||||||
        if isinstance(obj, dict):
 | 
					 | 
				
			||||||
            return {k: self._convert2serialize(v) for k, v in obj.items()}
 | 
					 | 
				
			||||||
        elif hasattr(obj, "_ast"):
 | 
					 | 
				
			||||||
            return self._convert2serialize(obj._ast())
 | 
					 | 
				
			||||||
        elif not isinstance(obj, str) and hasattr(obj, "__iter__"):
 | 
					 | 
				
			||||||
            return [self._convert2serialize(v) for v in obj]
 | 
					 | 
				
			||||||
        elif hasattr(obj, "__dict__"):
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                k: self._convert2serialize(v)
 | 
					 | 
				
			||||||
                for k, v in obj.__dict__.items()
 | 
					 | 
				
			||||||
                if not callable(v) and not k.startswith('_')
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return obj
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def new_update(self, event_type):
 | 
					    def new_update(self, event_type):
 | 
				
			||||||
        if event_type == "event_hms_errors":
 | 
					        if event_type == "event_hms_errors":
 | 
				
			||||||
            self._update_hms_errors()
 | 
					            self._update_hms_errors()
 | 
				
			||||||
@@ -196,13 +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
 | 
				
			||||||
        ams_data = self._convert2serialize(device_data.ams.data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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
 | 
				
			||||||
@@ -210,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 (
 | 
				
			||||||
@@ -245,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):
 | 
				
			||||||
@@ -277,11 +244,7 @@ class BambuVirtualPrinter:
 | 
				
			|||||||
            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"
 | 
					 | 
				
			||||||
                if self._settings.get_boolean(["local_mqtt"])
 | 
					 | 
				
			||||||
                else self._settings.get(["username"])
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            access_code=self._settings.get(["access_code"]),
 | 
					            access_code=self._settings.get(["access_code"]),
 | 
				
			||||||
            local_mqtt=self._settings.get_boolean(["local_mqtt"]),
 | 
					            local_mqtt=self._settings.get_boolean(["local_mqtt"]),
 | 
				
			||||||
            region=self._settings.get(["region"]),
 | 
					            region=self._settings.get(["region"]),
 | 
				
			||||||
@@ -357,7 +320,7 @@ class BambuVirtualPrinter:
 | 
				
			|||||||
            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._selected_project_file = file_info
 | 
					        self._selected_project_file = file_info
 | 
				
			||||||
@@ -367,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:
 | 
				
			||||||
@@ -470,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")
 | 
				
			||||||
@@ -685,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:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,552 +0,0 @@
 | 
				
			|||||||
from __future__ import annotations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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.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.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.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, device_type: str, serial: str, host: str, local_mqtt: bool, region: str, email: str,
 | 
					 | 
				
			||||||
                 username: str, auth_token: str, access_code: str, usage_hours: float = 0, manual_refresh_mode: bool = False):
 | 
					 | 
				
			||||||
        self.callback = None
 | 
					 | 
				
			||||||
        self.host = host
 | 
					 | 
				
			||||||
        self._local_mqtt = local_mqtt
 | 
					 | 
				
			||||||
        self._serial = serial
 | 
					 | 
				
			||||||
        self._auth_token = auth_token
 | 
					 | 
				
			||||||
        self._access_code = access_code
 | 
					 | 
				
			||||||
        self._username = username
 | 
					 | 
				
			||||||
        self._connected = False
 | 
					 | 
				
			||||||
        self._device_type = device_type
 | 
					 | 
				
			||||||
        self._usage_hours = usage_hours
 | 
					 | 
				
			||||||
        self._port = 1883
 | 
					 | 
				
			||||||
        self._refreshed = False
 | 
					 | 
				
			||||||
        self._manual_refresh_mode = manual_refresh_mode
 | 
					 | 
				
			||||||
        self._device = Device(self)
 | 
					 | 
				
			||||||
        self.bambu_cloud = BambuCloud(region, email, username, 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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE)
 | 
					 | 
				
			||||||
        self.client.tls_insecure_set(True)
 | 
					 | 
				
			||||||
        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 _on_connect(self):
 | 
					 | 
				
			||||||
        self._connected = True
 | 
					 | 
				
			||||||
        self.subscribe_and_request_info()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        LOGGER.debug("Starting watchdog thread")
 | 
					 | 
				
			||||||
        self._watchdog = WatchdogThread(self)
 | 
					 | 
				
			||||||
        self._watchdog.start()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self._device.supports_feature(Features.CAMERA_IMAGE):
 | 
					 | 
				
			||||||
            LOGGER.debug("Starting Chamber Image thread")
 | 
					 | 
				
			||||||
            self._camera = ChamberImageThread(self)
 | 
					 | 
				
			||||||
            self._camera.start()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
        if self._camera is not None:
 | 
					 | 
				
			||||||
            LOGGER.debug("Stopping camera thread")
 | 
					 | 
				
			||||||
            self._camera.stop()
 | 
					 | 
				
			||||||
            self._camera.join()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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:")
 | 
					 | 
				
			||||||
            LOGGER.error(f"Exception type: {type(e)}")
 | 
					 | 
				
			||||||
            LOGGER.error(f"Exception data: {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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE)
 | 
					 | 
				
			||||||
        self.client.tls_insecure_set(True)
 | 
					 | 
				
			||||||
        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,293 +0,0 @@
 | 
				
			|||||||
from __future__ import annotations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import base64
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import httpx
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from dataclasses import dataclass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from .const import LOGGER
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _get_authentication_token(self) -> dict:
 | 
					 | 
				
			||||||
        LOGGER.debug("Getting accessToken from Bambu Cloud")
 | 
					 | 
				
			||||||
        if self._region == "China":
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.cn/v1/user-service/user/login'
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.com/v1/user-service/user/login'
 | 
					 | 
				
			||||||
        headers = {'User-Agent' : "HA Bambulab"}
 | 
					 | 
				
			||||||
        data = {'account': self._email, 'password': self._password}
 | 
					 | 
				
			||||||
        with httpx.Client(http2=True) as client:
 | 
					 | 
				
			||||||
            response = client.post(url, headers=headers, json=data, timeout=10)
 | 
					 | 
				
			||||||
        if response.status_code >= 400:
 | 
					 | 
				
			||||||
            LOGGER.debug(f"Received error: {response.status_code}")
 | 
					 | 
				
			||||||
            raise ValueError(response.status_code)
 | 
					 | 
				
			||||||
        return response.json()['accessToken']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _get_username_from_authentication_token(self) -> str:
 | 
					 | 
				
			||||||
        # User name is in 2nd portion of the auth token (delimited with periods)
 | 
					 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
        return jsonAuthToken['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):
 | 
					 | 
				
			||||||
        self._region = region
 | 
					 | 
				
			||||||
        self._email = email
 | 
					 | 
				
			||||||
        self._password = password
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self._auth_token = self._get_authentication_token()
 | 
					 | 
				
			||||||
        self._username = self._get_username_from_authentication_token()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_device_list(self) -> dict:
 | 
					 | 
				
			||||||
        LOGGER.debug("Getting device list from Bambu Cloud")
 | 
					 | 
				
			||||||
        if self._region == "China":
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.cn/v1/iot-service/api/user/bind'
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.com/v1/iot-service/api/user/bind'
 | 
					 | 
				
			||||||
        headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"}
 | 
					 | 
				
			||||||
        with httpx.Client(http2=True) as client:
 | 
					 | 
				
			||||||
            response = client.get(url, headers=headers, timeout=10)
 | 
					 | 
				
			||||||
        if response.status_code >= 400:
 | 
					 | 
				
			||||||
            LOGGER.debug(f"Received error: {response.status_code}")
 | 
					 | 
				
			||||||
            raise ValueError(response.status_code)
 | 
					 | 
				
			||||||
        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")
 | 
					 | 
				
			||||||
        if self._region == "China":
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.cn/v1/iot-service/api/slicer/setting?version=undefined'
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=undefined'
 | 
					 | 
				
			||||||
        headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"}
 | 
					 | 
				
			||||||
        with httpx.Client(http2=True) as client:
 | 
					 | 
				
			||||||
            response = client.get(url, headers=headers, timeout=10)
 | 
					 | 
				
			||||||
        if response.status_code >= 400:
 | 
					 | 
				
			||||||
            LOGGER.error(f"Slicer settings load failed: {response.status_code}")
 | 
					 | 
				
			||||||
            return None
 | 
					 | 
				
			||||||
        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:
 | 
					 | 
				
			||||||
        if self._region == "China":
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.cn/v1/user-service/my/tasks'
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            url = 'https://api.bambulab.com/v1/user-service/my/tasks'
 | 
					 | 
				
			||||||
        headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"}
 | 
					 | 
				
			||||||
        with httpx.Client(http2=True) as client:
 | 
					 | 
				
			||||||
            response = client.get(url, headers=headers, timeout=10)
 | 
					 | 
				
			||||||
        if response.status_code >= 400:
 | 
					 | 
				
			||||||
            LOGGER.debug(f"Received error: {response.status_code}")
 | 
					 | 
				
			||||||
            raise ValueError(response.status_code)
 | 
					 | 
				
			||||||
        return response.json()
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def get_latest_task_for_printer(self, deviceId: str) -> dict:
 | 
					 | 
				
			||||||
        LOGGER.debug(f"Getting latest task from Bambu Cloud for Printer: {deviceId}")
 | 
					 | 
				
			||||||
        data = self.get_tasklist_for_printer(deviceId)
 | 
					 | 
				
			||||||
        if len(data) != 0:
 | 
					 | 
				
			||||||
            return data[0]
 | 
					 | 
				
			||||||
        LOGGER.debug("No tasks found for printer")
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_tasklist_for_printer(self, deviceId: str) -> dict:
 | 
					 | 
				
			||||||
        LOGGER.debug(f"Getting task list from Bambu Cloud for Printer: {deviceId}")
 | 
					 | 
				
			||||||
        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}")
 | 
					 | 
				
			||||||
        with httpx.Client(http2=True) as client:
 | 
					 | 
				
			||||||
            response = client.get(url, timeout=10)
 | 
					 | 
				
			||||||
        if response.status_code >= 400:
 | 
					 | 
				
			||||||
            LOGGER.debug(f"Received error: {response.status_code}")
 | 
					 | 
				
			||||||
            raise ValueError(response.status_code)
 | 
					 | 
				
			||||||
        return response.content
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def username(self):
 | 
					 | 
				
			||||||
        return self._username
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def auth_token(self):
 | 
					 | 
				
			||||||
        return self._auth_token
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    @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"}}
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,72 +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",
 | 
					 | 
				
			||||||
    "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",
 | 
					 | 
				
			||||||
    "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",
 | 
					 | 
				
			||||||
    "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",
 | 
					 | 
				
			||||||
    "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",
 | 
					 | 
				
			||||||
    "GFT97": "Generic PPS",
 | 
					 | 
				
			||||||
    "GFT98": "Generic PPS-CF",
 | 
					 | 
				
			||||||
    "GFU00": "Bambu TPU 95A HF",
 | 
					 | 
				
			||||||
    "GFU01": "Bambu TPU 95A",
 | 
					 | 
				
			||||||
    "GFU99": "Generic TPU"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,227 +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,
 | 
					 | 
				
			||||||
    FansEnum,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
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 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)
 | 
					 | 
				
			||||||
@@ -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
 | 
				
			||||||
@@ -75,19 +75,19 @@ class PrintingState(APrinterState):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        progress = print_job_info.print_percentage
 | 
					        progress = print_job_info.print_percentage
 | 
				
			||||||
        self._printer.current_print_job = PrintJob(project_file_info, progress, print_job_info.remaining_time, print_job_info.current_layer, print_job_info.total_layers)
 | 
					        self._printer.current_print_job = PrintJob(project_file_info, progress)
 | 
				
			||||||
        self._printer.select_project_file(project_file_info.path.as_posix())
 | 
					        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,29 +15,6 @@ $(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.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("");
 | 
				
			||||||
            OctoPrint.simpleApiCommand("bambu_printer", "register", {
 | 
					            OctoPrint.simpleApiCommand("bambu_printer", "register", {
 | 
				
			||||||
@@ -91,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.settingsViewModel.settings.plugins.bambu_printer.flow_cali($('#bambu_printer_flow_cali').is(':checked'));
 | 
				
			||||||
 | 
					                        self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali($('#bambu_printer_vibration_cali').is(':checked'));
 | 
				
			||||||
 | 
					                        self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect($('#bambu_printer_layer_inspect').is(':checked'));
 | 
				
			||||||
 | 
					                        self.settingsViewModel.settings.plugins.bambu_printer.use_ams($('#bambu_printer_use_ams').is(':checked'));
 | 
				
			||||||
 | 
					                        self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options(true);
 | 
				
			||||||
 | 
					                        self.settingsViewModel.saveData();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
        };
 | 
					                    // replace this with our own print command API call?
 | 
				
			||||||
 | 
					                    start_print_command();
 | 
				
			||||||
        self.cancel_print_options = function() {
 | 
					                },
 | 
				
			||||||
            self.settingsViewModel.settings.plugins.bambu_printer.use_ams(self.use_ams);
 | 
					                nofade: true
 | 
				
			||||||
            $("#bambu_printer_print_options").modal('hide');
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.accept_print_options = function() {
 | 
					 | 
				
			||||||
            console.log("starting print!!!!");
 | 
					 | 
				
			||||||
            console.log(self.ams_mapping());
 | 
					 | 
				
			||||||
            $("#bambu_printer_print_options").modal('hide');
 | 
					 | 
				
			||||||
            var flattened_ams_mapping = ko.utils.arrayMap(self.ams_mapping(), function(item) {
 | 
					 | 
				
			||||||
                return item.index();
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            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>
 | 
					 | 
				
			||||||
@@ -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.8rc4"
 | 
					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", "httpx[http2]>=0.27.0"]
 | 
					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", "httpx[http2]>=0.27.0"]
 | 
				
			|||||||
# 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