diff --git a/octoprint_bambu_printer/bambu_print_plugin.py b/octoprint_bambu_printer/bambu_print_plugin.py index 1db0b9c..ce23544 100644 --- a/octoprint_bambu_printer/bambu_print_plugin.py +++ b/octoprint_bambu_printer/bambu_print_plugin.py @@ -108,6 +108,11 @@ def get_settings_defaults(self): "ams_current_tray": 255, } + def on_settings_save(self, data): + if data.get("local_mqtt", False) is True: + data["auth_token"] = "" + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) + def is_api_adminonly(self): return True diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index 60f3c0c..9a83e71 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -345,21 +345,21 @@ def remove_project_selection(self): self._selected_project_file = None def select_project_file(self, file_path: str) -> bool: - self._log.debug(f"Select project file: {file_path}") - file_info = self._project_files_view.get_file_by_stem( - file_path, [".gcode", ".3mf"] - ) + file_info = self._project_files_view.get_file_by_name(file_path) if ( self._selected_project_file is not None and file_info is not None and self._selected_project_file.path == file_info.path ): + self._log.debug(f"File already selected: {file_path}") return True if file_info is None: self._log.error(f"Cannot select non-existent file: {file_path}") return False + self._log.debug(f"Select project file: {file_path}") + self._selected_project_file = file_info self._send_file_selected_message() return True diff --git a/octoprint_bambu_printer/printer/file_system/cached_file_view.py b/octoprint_bambu_printer/printer/file_system/cached_file_view.py index a4e2074..c1e1d0b 100644 --- a/octoprint_bambu_printer/printer/file_system/cached_file_view.py +++ b/octoprint_bambu_printer/printer/file_system/cached_file_view.py @@ -35,8 +35,8 @@ def list_all_views(self): result: list[FileInfo] = [] with self.file_system.get_ftps_client() as ftp: - for filter in self.folder_view.keys(): - result.extend(self.file_system.list_files(*filter, ftp, existing_files)) + for key in self.folder_view.keys(): + result.extend(self.file_system.list_files(*key, ftp, existing_files)) return result def update(self): @@ -56,6 +56,9 @@ def get_all_info(self): def get_all_cached_info(self): return list(self._file_data_cache.values()) + def get_keys_as_list(self): + return list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()) + def get_file_data(self, file_path: str | Path) -> FileInfo | None: file_data = self.get_file_data_cached(file_path) if file_data is None: @@ -73,22 +76,19 @@ def get_file_data_cached(self, file_path: str | Path) -> FileInfo | None: file_path = self._file_alias_cache.get(file_path, file_path) return self._file_data_cache.get(file_path, None) - def get_file_by_stem(self, file_stem: str, allowed_suffixes: list[str]): - if file_stem == "": + def get_file_by_name(self, file_name: str): + if file_name == "": return None - file_stem = Path(file_stem).with_suffix("").stem - file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) + file_list = self.get_keys_as_list() + if not file_name in file_list: + if f"{file_name}.3mf" in file_list: + file_name = f"{file_name}.3mf" + elif f"{file_name}.gcode.3mf" in file_list: + file_name = f"{file_name}.gcode.3mf" + + file_data = self.get_file_data_cached(file_name) if file_data is None: self.update() - file_data = self._get_file_by_stem_cached(file_stem, allowed_suffixes) + return self.get_file_by_name(file_name) return file_data - - def _get_file_by_stem_cached(self, file_stem: str, allowed_suffixes: list[str]): - for file_path_str in list(self._file_data_cache.keys()) + list(self._file_alias_cache.keys()): - file_path = Path(file_path_str) - if file_stem == file_path.with_suffix("").stem and any( - suffix in allowed_suffixes for suffix in file_path.suffixes - ): - return self.get_file_data_cached(file_path) - return None diff --git a/octoprint_bambu_printer/printer/pybambu/bambu_client.py b/octoprint_bambu_printer/printer/pybambu/bambu_client.py index a0cd9a4..f9d0175 100644 --- a/octoprint_bambu_printer/printer/pybambu/bambu_client.py +++ b/octoprint_bambu_printer/printer/pybambu/bambu_client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import queue import json import math @@ -36,6 +37,7 @@ def __init__(self, client): self._stop_event = threading.Event() self._last_received_data = time.time() super().__init__() + self.daemon = True self.setName(f"{self._client._device.info.device_type}-Watchdog-{threading.get_native_id()}") def stop(self): @@ -70,6 +72,7 @@ def __init__(self, client): self._client = client self._stop_event = threading.Event() super().__init__() + self.daemon = True self.setName(f"{self._client._device.info.device_type}-Chamber-{threading.get_native_id()}") def stop(self): @@ -178,7 +181,7 @@ def run(self): # Reset buffer img = None - # else: + # else: # Otherwise we need to continue looping without reseting the buffer to receive the remaining data # and without delaying. @@ -223,6 +226,7 @@ def __init__(self, client): self._client = client self._stop_event = threading.Event() super().__init__() + self.daemon = True self.setName(f"{self._client._device.info.device_type}-Mqtt-{threading.get_native_id()}") def stop(self): @@ -282,7 +286,7 @@ class BambuClient: _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): + username: str, auth_token: str, access_code: str, usage_hours: float = 0, manual_refresh_mode: bool = False, chamber_image: bool = True): self.callback = None self.host = host self._local_mqtt = local_mqtt @@ -299,6 +303,7 @@ def __init__(self, device_type: str, serial: str, host: str, local_mqtt: bool, r self._device = Device(self) self.bambu_cloud = BambuCloud(region, email, username, auth_token) self.slicer_settings = SlicerSettings(self) + self.use_chamber_image = chamber_image @property def connected(self): @@ -319,6 +324,10 @@ async def set_manual_refresh_mode(self, on): # Reconnect normally self.connect(self.callback) + def setup_tls(self): + self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE) + self.client.tls_insecure_set(True) + def connect(self, callback): """Connect to the MQTT Broker""" self.client = mqtt.Client() @@ -329,8 +338,9 @@ def connect(self, callback): # 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) + # Run the blocking tls_set method in a separate thread + self.setup_tls() + self._port = 8883 if self._local_mqtt: self.client.username_pw_set("bblp", password=self._access_code) @@ -369,10 +379,14 @@ def _on_connect(self): 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() + if not self._device.supports_feature(Features.CAMERA_RTSP): + if self._device.supports_feature(Features.CAMERA_IMAGE): + if self.use_chamber_image: + LOGGER.debug("Starting Chamber Image thread") + self._camera = ChamberImageThread(self) + self._camera.start() + elif (self.host == "") or (self._access_code == ""): + LOGGER.debug("Skipping camera setup as local access details not provided.") def try_on_connect(self, client_: mqtt.Client, @@ -396,7 +410,7 @@ def on_disconnect(self, """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 @@ -451,9 +465,7 @@ def on_message(self, client, userdata, message): 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}") + LOGGER.error("An exception occurred processing a message:", exc_info=e) def subscribe(self): """Subscribe to report topic""" @@ -516,8 +528,10 @@ def on_message(client, userdata, message): 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) + # Run the blocking tls_set method in a separate thread + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self.setup_tls) + if self._local_mqtt: self.client.username_pw_set("bblp", password=self._access_code) else: diff --git a/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py b/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py index 0174715..4d8b4c3 100644 --- a/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py +++ b/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py @@ -2,36 +2,148 @@ import base64 import json -import httpx + +from curl_cffi import requests from dataclasses import dataclass -from .const import LOGGER +from .const import ( + LOGGER, + BambuUrl +) + +from .utils import get_Url + +IMPERSONATE_BROWSER='chrome' @dataclass class BambuCloud: - + def __init__(self, region: str, email: str, username: str, auth_token: str): self._region = region self._email = email self._username = username self._auth_token = auth_token + self._tfaKey = None + def _get_headers_with_auth_token(self) -> dict: + headers = {} + headers['Authorization'] = f"Bearer {self._auth_token}" + return headers + 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' : "OctoPrint Plugin"} - data = {'account': self._email, 'password': self._password} - with httpx.Client(http2=True) as client: - response = client.post(url, headers=headers, json=data, timeout=10) + + # First we need to find out how Bambu wants us to login. + data = { + "account": self._email, + "password": self._password, + "apiError": "" + } + + response = requests.post(get_Url(BambuUrl.LOGIN, self._region), json=data, impersonate=IMPERSONATE_BROWSER) if response.status_code >= 400: - LOGGER.debug(f"Received error: {response.status_code}") + LOGGER.error(f"Login attempt failed with error code: {response.status_code}") + LOGGER.debug(f"Response: '{response.text}'") + raise ValueError(response.status_code) + + LOGGER.debug(f"Response: {response.status_code}") + + auth_json = response.json() + accessToken = auth_json.get('accessToken', '') + if accessToken != '': + # We were provided the accessToken directly. + return accessToken + + loginType = auth_json.get("loginType", None) + if loginType is None: + LOGGER.error(f"loginType not present") + LOGGER.error(f"Response not understood: '{response.text}'") + return None + elif loginType == 'verifyCode': + LOGGER.debug(f"Received verifyCode response") + elif loginType == 'tfa': + # Store the tfaKey for later use + LOGGER.debug(f"Received tfa response") + self._tfaKey = auth_json.get("tfaKey") + else: + LOGGER.debug(f"Did not understand json. loginType = '{loginType}'") + LOGGER.error(f"Response not understood: '{response.text}'") + + return loginType + + def _get_email_verification_code(self): + # Send the verification code request + data = { + "email": self._email, + "type": "codeLogin" + } + + LOGGER.debug("Requesting verification code") + response = requests.post(get_Url(BambuUrl.EMAIL_CODE, self._region), json=data, impersonate=IMPERSONATE_BROWSER) + + if response.status_code == 200: + LOGGER.debug("Verification code requested successfully.") + else: + LOGGER.error(f"Received error trying to send verification code: {response.status_code}") + LOGGER.debug(f"Response: '{response.text}'") + raise ValueError(response.status_code) + + def _get_authentication_token_with_verification_code(self, code) -> dict: + LOGGER.debug("Attempting to connect with provided verification code.") + data = { + "account": self._email, + "code": code + } + + response = requests.post(get_Url(BambuUrl.LOGIN, self._region), json=data, impersonate=IMPERSONATE_BROWSER) + + LOGGER.debug(f"Response: {response.status_code}") + if response.status_code == 200: + LOGGER.debug("Authentication successful.") + elif response.status_code == 400: + LOGGER.debug(f"Response: '{response.json()}'") + if response.json()['code'] == 1: + # Code has expired. Request a new one. + self._get_email_verification_code() + return 'codeExpired' + elif response.json()['code'] == 2: + # Code was incorrect. Let the user try again. + return 'codeIncorrect' + else: + LOGGER.error(f"Response not understood: '{response.json()}'") + raise ValueError(response.json()['code']) + else: + LOGGER.error(f"Received error trying to authenticate with verification code: {response.status_code}") + LOGGER.debug(f"Response: '{response.text}'") raise ValueError(response.status_code) + return response.json()['accessToken'] + + def _get_authentication_token_with_2fa_code(self, code: str) -> dict: + LOGGER.debug("Attempting to connect with provided 2FA code.") + + data = { + "tfaKey": self._tfaKey, + "tfaCode": code + } + + response = requests.post(get_Url(BambuUrl.TFA_LOGIN, self._region), json=data, impersonate=IMPERSONATE_BROWSER) + + LOGGER.debug(f"Response: {response.status_code}") + if response.status_code == 200: + LOGGER.debug("Authentication successful.") + else: + LOGGER.error(f"Received error trying to authenticate with verification code: {response.status_code}") + LOGGER.debug(f"Response: '{response.text}'") + raise ValueError(response.status_code) + + cookies = response.cookies.get_dict() + token_from_tfa = cookies.get("token") + LOGGER.debug(f"token_from_tfa: {token_from_tfa}") + return token_from_tfa + def _get_username_from_authentication_token(self) -> str: # User name is in 2nd portion of the auth token (delimited with periods) b64_string = self._auth_token.split(".")[1] @@ -40,7 +152,7 @@ def _get_username_from_authentication_token(self) -> str: jsonAuthToken = json.loads(base64.b64decode(b64_string)) # Gives json payload with "username":"u_" within it return jsonAuthToken['username'] - + # Retrieves json description of devices in the form: # { # 'message': 'success', @@ -79,7 +191,7 @@ def _get_username_from_authentication_token(self) -> str: # } # ] # } - + def test_authentication(self, region: str, email: str, username: str, auth_token: str) -> bool: self._region = region self._email = email @@ -91,23 +203,41 @@ def test_authentication(self, region: str, email: str, username: str, auth_token return False return True - def login(self, region: str, email: str, password: str): + def login(self, region: str, email: str, password: str) -> str: self._region = region self._email = email self._password = password - self._auth_token = self._get_authentication_token() + result = self._get_authentication_token() + if result == 'verifyCode': + return result + elif result == 'tfa': + return result + elif result is None: + LOGGER.error("Unable to authenticate.") + return None + else: + self._auth_token = result + self._username = self._get_username_from_authentication_token() + return 'success' + + def login_with_verification_code(self, code: str): + result = self._get_authentication_token_with_verification_code(code) + if result == 'codeExpired' or result == 'codeIncorrect': + return result + self._auth_token = result + self._username = self._get_username_from_authentication_token() + return 'success' + + def login_with_2fa_code(self, code: str): + result = self._get_authentication_token_with_2fa_code(code) + self._auth_token = result self._username = self._get_username_from_authentication_token() + return 'success' 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' : "OctoPrint Plugin"} - with httpx.Client(http2=True) as client: - response = client.get(url, headers=headers, timeout=10) + response = requests.get(get_Url(BambuUrl.BIND, self._region), headers=self._get_headers_with_auth_token(), timeout=10, impersonate=IMPERSONATE_BROWSER) if response.status_code >= 400: LOGGER.debug(f"Received error: {response.status_code}") raise ValueError(response.status_code) @@ -182,18 +312,13 @@ def get_device_list(self) -> dict: 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' : "OctoPrint Plugin"} - with httpx.Client(http2=True) as client: - response = client.get(url, headers=headers, timeout=10) + response = requests.get(get_Url(BambuUrl.SLICER_SETTINGS, self._region), headers=self._get_headers_with_auth_token(), timeout=10, impersonate=IMPERSONATE_BROWSER) if response.status_code >= 400: LOGGER.error(f"Slicer settings load failed: {response.status_code}") + LOGGER.error(f"Slicer settings load failed: '{response.text}'") return None return response.json() - + # The task list is of the following form with a 'hits' array with typical 20 entries. # # "total": 531, @@ -237,20 +362,16 @@ def get_slicer_settings(self) -> dict: # }, 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' : "OctoPrint Plugin"} - with httpx.Client(http2=True) as client: - response = client.get(url, headers=headers, timeout=10) + url = get_Url(BambuUrl.TASKS, self._region) + response = requests.get(url, headers=self._get_headers_with_auth_token(), timeout=10, impersonate=IMPERSONATE_BROWSER) if response.status_code >= 400: LOGGER.debug(f"Received error: {response.status_code}") + LOGGER.debug(f"Received error: '{response.text}'") 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}") + LOGGER.debug(f"Getting latest task from Bambu Cloud") data = self.get_tasklist_for_printer(deviceId) if len(data) != 0: return data[0] @@ -258,7 +379,7 @@ def get_latest_task_for_printer(self, deviceId: str) -> dict: return None def get_tasklist_for_printer(self, deviceId: str) -> dict: - LOGGER.debug(f"Getting task list from Bambu Cloud for Printer: {deviceId}") + LOGGER.debug(f"Getting task list from Bambu Cloud") tasks = [] data = self.get_tasklist() for task in data['hits']: @@ -273,8 +394,7 @@ def get_device_type_from_device_product_name(self, device_product_name: str): 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) + response = requests.get(url, timeout=10, impersonate=IMPERSONATE_BROWSER) if response.status_code >= 400: LOGGER.debug(f"Received error: {response.status_code}") raise ValueError(response.status_code) @@ -283,11 +403,11 @@ def download(self, url: str) -> bytearray: @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" diff --git a/octoprint_bambu_printer/printer/pybambu/const.py b/octoprint_bambu_printer/printer/pybambu/const.py index c867b14..878a748 100644 --- a/octoprint_bambu_printer/printer/pybambu/const.py +++ b/octoprint_bambu_printer/printer/pybambu/const.py @@ -27,6 +27,7 @@ class Features(Enum): CAMERA_IMAGE = 15, DOOR_SENSOR = 16, MANUAL_MODE = 17, + AMS_FILAMENT_REMAINING = 18, class FansEnum(Enum): @@ -1220,3 +1221,19 @@ class Home_Flag_Values(IntEnum): SUPPORTED_PLUS = 0x08000000, # Gap +class BambuUrl(Enum): + LOGIN = 1, + TFA_LOGIN = 2, + EMAIL_CODE = 3, + BIND = 4, + SLICER_SETTINGS = 5, + TASKS = 6, + +BAMBU_URL = { + BambuUrl.LOGIN: 'https://api.bambulab.com/v1/user-service/user/login', + BambuUrl.TFA_LOGIN: 'https://bambulab.com/api/sign-in/tfa', + BambuUrl.EMAIL_CODE: 'https://api.bambulab.com/v1/user-service/user/sendemail/code', + BambuUrl.BIND: 'https://api.bambulab.com/v1/iot-service/api/user/bind', + BambuUrl.SLICER_SETTINGS: 'https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=undefined', + BambuUrl.TASKS: 'https://api.bambulab.com/v1/user-service/my/tasks', +} diff --git a/octoprint_bambu_printer/printer/pybambu/models.py b/octoprint_bambu_printer/printer/pybambu/models.py index 398d0be..2e36eb8 100644 --- a/octoprint_bambu_printer/printer/pybambu/models.py +++ b/octoprint_bambu_printer/printer/pybambu/models.py @@ -78,7 +78,6 @@ def print_update(self, data) -> bool: send_event = send_event | self.home_flag.print_update(data = data) if send_event and self._client.callback is not None: - LOGGER.debug("event_printer_data_update") self._client.callback("event_printer_data_update") if data.get("msg", 0) == 0: @@ -93,7 +92,7 @@ def info_update(self, data): def supports_feature(self, feature): if feature == Features.AUX_FAN: - return True + return self.info.device_type != "A1" and self.info.device_type != "A1MINI" elif feature == Features.CHAMBER_LIGHT: return True elif feature == Features.CHAMBER_FAN: @@ -124,6 +123,9 @@ def supports_feature(self, feature): return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" elif feature == Features.MANUAL_MODE: return self.info.device_type == "P1P" or self.info.device_type == "P1S" or self.info.device_type == "A1" or self.info.device_type == "A1MINI" + elif feature == Features.AMS_FILAMENT_REMAINING: + # Technically this is not the AMS Lite but that's currently tied to only these printer types. + return self.info.device_type != "A1" and self.info.device_type != "A1MINI" return False @@ -384,7 +386,7 @@ def get_ams_print_weights(self) -> float: values = {} for i in range(16): if self._ams_print_weights[i] != 0: - values[f"AMS Slot {i}"] = self._ams_print_weights[i] + values[f"AMS Slot {i+1}"] = self._ams_print_weights[i] return values @property @@ -392,7 +394,7 @@ def get_ams_print_lengths(self) -> float: values = {} for i in range(16): if self._ams_print_lengths[i] != 0: - values[f"AMS Slot {i}"] = self._ams_print_lengths[i] + values[f"AMS Slot {i+1}"] = self._ams_print_lengths[i] return values def __init__(self, client): @@ -450,7 +452,8 @@ def print_update(self, data) -> bool: self.gcode_file = data.get("gcode_file", self.gcode_file) self.print_type = data.get("print_type", self.print_type) if self.print_type.lower() not in PRINT_TYPE_OPTIONS: - LOGGER.debug(f"Unknown print_type. Please log an issue : '{self.print_type}'") + if self.print_type != "": + LOGGER.debug(f"Unknown print_type. Please log an issue : '{self.print_type}'") self.print_type = "unknown" self.subtask_name = data.get("subtask_name", self.subtask_name) self.file_type_icon = "mdi:file" if self.print_type != "cloud" else "mdi:cloud-outline" @@ -471,9 +474,7 @@ def print_update(self, data) -> bool: if data.get("mc_remaining_time") is not None: existing_remaining_time = self.remaining_time self.remaining_time = data.get("mc_remaining_time") - if self.start_time is None: - self.end_time = None - elif existing_remaining_time != self.remaining_time: + if existing_remaining_time != self.remaining_time: self.end_time = get_end_time(self.remaining_time) LOGGER.debug(f"END TIME2: {self.end_time}") @@ -796,6 +797,12 @@ def has_bambu_cloud_connection(self) -> bool: @dataclass class AMSInstance: """Return all AMS instance related info""" + serial: str + sw_version: str + hw_version: str + humidity_index: int + temperature: int + tray: list["AMSTray"] def __init__(self, client): self.serial = "" @@ -813,11 +820,14 @@ def __init__(self, client): @dataclass class AMSList: """Return all AMS related info""" + tray_now: int + data: list[AMSInstance] def __init__(self, client): self._client = client self.tray_now = 0 self.data = [None] * 4 + self._first_initialization_done = False def info_update(self, data): old_data = f"{self.__dict__}" @@ -859,7 +869,7 @@ def info_update(self, data): if index != -1: # Sometimes we get incomplete version data. We have to skip if that occurs since the serial number is - # requires as part of the home assistant device identity. + # required as part of the home assistant device identity. if not module['sn'] == '': # May get data before info so create entries if necessary if self.data[index] is None: @@ -874,6 +884,9 @@ def info_update(self, data): if self.data[index].hw_version != module['hw_ver']: data_changed = True self.data[index].hw_version = module['hw_ver'] + elif not self._first_initialization_done: + self._first_initialization_done = True + data_changed = True data_changed = data_changed or (old_data != f"{self.__dict__}") @@ -969,6 +982,19 @@ def print_update(self, data) -> bool: @dataclass class AMSTray: """Return all AMS tray related info""" + empty: bool + idx: int + name: str + type: str + sub_brands: str + color: str + nozzle_temp_min: int + nozzle_temp_max: int + remain: int + k: float + tag_uid: str + tray_uuid: str + def __init__(self, client): self._client = client @@ -982,7 +1008,8 @@ def __init__(self, client): self.nozzle_temp_max = 0 self.remain = 0 self.k = 0 - self.tag_uid = "0000000000000000" + self.tag_uid = "" + self.tray_uuid = "" def print_update(self, data) -> bool: old_data = f"{self.__dict__}" @@ -998,7 +1025,8 @@ def print_update(self, data) -> bool: self.nozzle_temp_min = 0 self.nozzle_temp_max = 0 self.remain = 0 - self.tag_uid = "0000000000000000" + self.tag_uid = "" + self.tray_uuid = "" self.k = 0 else: self.empty = False @@ -1011,6 +1039,7 @@ def print_update(self, data) -> bool: self.nozzle_temp_max = data.get('nozzle_temp_max', self.nozzle_temp_max) self.remain = data.get('remain', self.remain) self.tag_uid = data.get('tag_uid', self.tag_uid) + self.tray_uuid = data.get('tray_uuid', self.tray_uuid) self.k = data.get('k', self.k) return (old_data != f"{self.__dict__}") @@ -1191,8 +1220,9 @@ class PrintErrorList: _count: int def __init__(self, client): + self._error = None + self._count = 0 self._client = client - self._error = {} def print_update(self, data) -> bool: # Example payload: @@ -1202,7 +1232,7 @@ def print_update(self, data) -> bool: # 'Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.' if 'print_error' in data.keys(): - errors = {} + errors = None print_error_code = data.get('print_error') if print_error_code != 0: hex_conversion = f'0{int(print_error_code):x}' @@ -1232,6 +1262,8 @@ def on(self) -> int: @dataclass class HMSNotification: """Return an HMS object and all associated details""" + attr: int + code: int def __init__(self, attr: int = 0, code: int = 0): self.attr = attr diff --git a/octoprint_bambu_printer/printer/pybambu/utils.py b/octoprint_bambu_printer/printer/pybambu/utils.py index 4b71824..f2a0cf5 100644 --- a/octoprint_bambu_printer/printer/pybambu/utils.py +++ b/octoprint_bambu_printer/printer/pybambu/utils.py @@ -12,6 +12,7 @@ HMS_MODULES, LOGGER, FansEnum, + BAMBU_URL ) from .commands import SEND_GCODE_TEMPLATE @@ -59,8 +60,8 @@ def get_filament_name(idx, custom_filaments: dict): 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}'") + # if result == "unknown" and idx != "": + # LOGGER.debug(f"UNKNOWN FILAMENT IDX: '{idx}'") return result @@ -225,3 +226,10 @@ def round_minute(date: datetime = None, round_to: int = 1): date = date.replace(second=0, microsecond=0) delta = date.minute % round_to return date.replace(minute=date.minute - delta) + + +def get_Url(url: str, region: str): + urlstr = BAMBU_URL[url] + if region == "China": + urlstr = urlstr.replace('.com', '.cn') + return urlstr diff --git a/octoprint_bambu_printer/printer/states/printing_state.py b/octoprint_bambu_printer/printer/states/printing_state.py index a83c627..d78c9f1 100644 --- a/octoprint_bambu_printer/printer/states/printing_state.py +++ b/octoprint_bambu_printer/printer/states/printing_state.py @@ -22,7 +22,7 @@ class PrintingState(APrinterState): def __init__(self, printer: BambuVirtualPrinter) -> None: super().__init__(printer) - self._current_print_job = None + self._printer.current_print_job = None self._is_printing = False self._sd_printing_thread = None @@ -40,12 +40,15 @@ def finalize(self): self._printer.current_print_job = None def _start_worker_thread(self): + self._is_printing = True if self._sd_printing_thread is None: - self._is_printing = True self._sd_printing_thread = threading.Thread(target=self._printing_worker) self._sd_printing_thread.start() + else: + self._sd_printing_thread.join() def _printing_worker(self): + self._log.debug(f"_printing_worker before while loop: {self._printer.current_print_job}") while ( self._is_printing and self._printer.current_print_job is not None @@ -55,6 +58,7 @@ def _printing_worker(self): self._printer.report_print_job_status() time.sleep(3) + self._log.debug(f"_printing_worker after while loop: {self._printer.current_print_job}") self.update_print_job_info() if ( self._printer.current_print_job is not None @@ -64,13 +68,15 @@ def _printing_worker(self): def update_print_job_info(self): print_job_info = self._printer.bambu_client.get_device().print_job - task_name: str = print_job_info.subtask_name - project_file_info = self._printer.project_files.get_file_by_stem( - task_name, [".gcode", ".3mf"] - ) + subtask_name: str = print_job_info.subtask_name + gcode_file: str = print_job_info.gcode_file + + self._log.info(f"update_print_job_info: {print_job_info}") + + project_file_info = self._printer.project_files.get_file_by_name(subtask_name) or self._printer.project_files.get_file_by_name(gcode_file) if project_file_info is None: self._log.debug(f"No 3mf file found for {print_job_info}") - self._current_print_job = None + self._printer.current_print_job = None self._printer.change_state(self._printer._state_idle) return diff --git a/setup.py b/setup.py index 8d7b698..2012b34 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "OctoPrint-BambuPrinter" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.1.8rc6" +plugin_version = "0.1.8rc7" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module @@ -33,7 +33,7 @@ plugin_license = "AGPLv3" # 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", "curl_cffi"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point