ClusterUM3OutputDevice.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary).
  4. from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously.
  5. from UM.Logger import Logger
  6. from UM.Application import Application
  7. from UM.Settings.ContainerRegistry import ContainerRegistry
  8. from UM.i18n import i18nCatalog
  9. from UM.Message import Message
  10. from UM.Qt.Duration import Duration, DurationFormat
  11. from UM.OutputDevice import OutputDeviceError #To show that something went wrong when writing.
  12. from UM.Scene.SceneNode import SceneNode #For typing.
  13. from UM.Version import Version #To check against firmware versions for support.
  14. from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
  15. from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
  16. from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
  17. from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
  18. from cura.PrinterOutput.NetworkCamera import NetworkCamera
  19. from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
  20. from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
  21. from PyQt5.QtGui import QDesktopServices
  22. from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
  23. from time import time, sleep
  24. from datetime import datetime
  25. from typing import Optional, Dict, List
  26. import io #To create the correct buffers for sending data to the printer.
  27. import json
  28. import os
  29. i18n_catalog = i18nCatalog("cura")
  30. class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
  31. printJobsChanged = pyqtSignal()
  32. activePrinterChanged = pyqtSignal()
  33. # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
  34. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
  35. clusterPrintersChanged = pyqtSignal()
  36. def __init__(self, device_id, address, properties, parent = None):
  37. super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
  38. self._api_prefix = "/cluster-api/v1/"
  39. self._number_of_extruders = 2
  40. self._dummy_lambdas = set()
  41. self._print_jobs = []
  42. self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
  43. self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
  44. # See comments about this hack with the clusterPrintersChanged signal
  45. self.printersChanged.connect(self.clusterPrintersChanged)
  46. self._accepts_commands = True
  47. # Cluster does not have authentication, so default to authenticated
  48. self._authentication_state = AuthState.Authenticated
  49. self._error_message = None
  50. self._write_job_progress_message = None
  51. self._progress_message = None
  52. self._active_printer = None # type: Optional[PrinterOutputModel]
  53. self._printer_selection_dialog = None
  54. self.setPriority(3) # Make sure the output device gets selected above local file output
  55. self.setName(self._id)
  56. self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
  57. self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
  58. self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))
  59. self._printer_uuid_to_unique_name_mapping = {}
  60. self._finished_jobs = []
  61. self._cluster_size = int(properties.get(b"cluster_size", 0))
  62. self._latest_reply_handler = None
  63. def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
  64. self.writeStarted.emit(self)
  65. #Formats supported by this application (file types that we can actually write).
  66. if file_handler:
  67. file_formats = file_handler.getSupportedFileTypesWrite()
  68. else:
  69. file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
  70. #Create a list from the supported file formats string.
  71. machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
  72. machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
  73. #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
  74. if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"):
  75. machine_file_formats = ["application/x-ufp"] + machine_file_formats
  76. # Take the intersection between file_formats and machine_file_formats.
  77. format_by_mimetype = {format["mime_type"]: format for format in file_formats}
  78. file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
  79. if len(file_formats) == 0:
  80. Logger.log("e", "There are no file formats available to write with!")
  81. raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
  82. preferred_format = file_formats[0]
  83. #Just take the first file format available.
  84. if file_handler is not None:
  85. writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
  86. else:
  87. writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])
  88. #This function pauses with the yield, waiting on instructions on which printer it needs to print with.
  89. self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
  90. self._sending_job.send(None) #Start the generator.
  91. if len(self._printers) > 1: #We need to ask the user.
  92. self._spawnPrinterSelectionDialog()
  93. is_job_sent = True
  94. else: #Just immediately continue.
  95. self._sending_job.send("") #No specifically selected printer.
  96. is_job_sent = self._sending_job.send(None)
  97. def _spawnPrinterSelectionDialog(self):
  98. if self._printer_selection_dialog is None:
  99. path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
  100. self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
  101. if self._printer_selection_dialog is not None:
  102. self._printer_selection_dialog.show()
  103. @pyqtProperty(int, constant=True)
  104. def clusterSize(self):
  105. return self._cluster_size
  106. ## Allows the user to choose a printer to print with from the printer
  107. # selection dialogue.
  108. # \param target_printer The name of the printer to target.
  109. @pyqtSlot(str)
  110. def selectPrinter(self, target_printer: str = "") -> None:
  111. self._sending_job.send(target_printer)
  112. ## Greenlet to send a job to the printer over the network.
  113. #
  114. # This greenlet gets called asynchronously in requestWrite. It is a
  115. # greenlet in order to optionally wait for selectPrinter() to select a
  116. # printer.
  117. # The greenlet yields exactly three times: First time None,
  118. # \param writer The file writer to use to create the data.
  119. # \param preferred_format A dictionary containing some information about
  120. # what format to write to. This is necessary to create the correct buffer
  121. # types and file extension and such.
  122. def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
  123. Logger.log("i", "Sending print job to printer.")
  124. if self._sending_gcode:
  125. self._error_message = Message(
  126. i18n_catalog.i18nc("@info:status",
  127. "Sending new jobs (temporarily) blocked, still sending the previous print job."))
  128. self._error_message.show()
  129. yield #Wait on the user to select a target printer.
  130. yield #Wait for the write job to be finished.
  131. yield False #Return whether this was a success or not.
  132. yield #Prevent StopIteration.
  133. self._sending_gcode = True
  134. target_printer = yield #Potentially wait on the user to select a target printer.
  135. # Using buffering greatly reduces the write time for many lines of gcode
  136. if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
  137. stream = io.StringIO()
  138. else: #Binary mode.
  139. stream = io.BytesIO()
  140. job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])
  141. self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
  142. title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
  143. self._write_job_progress_message.show()
  144. self._dummy_lambdas = (target_printer, preferred_format, stream)
  145. job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)
  146. job.start()
  147. yield True #Return that we had success!
  148. yield #To prevent having to catch the StopIteration exception.
  149. from cura.Utils.Threading import call_on_qt_thread
  150. def _sendPrintJobWaitOnWriteJobFinished(self, job):
  151. self._write_job_progress_message.hide()
  152. self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
  153. title = i18n_catalog.i18nc("@info:title", "Sending Data"))
  154. self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
  155. self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
  156. self._progress_message.show()
  157. parts = []
  158. target_printer, preferred_format, stream = self._dummy_lambdas
  159. # If a specific printer was selected, it should be printed with that machine.
  160. if target_printer:
  161. target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
  162. parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
  163. # Add user name to the print_job
  164. parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
  165. file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
  166. output = stream.getvalue() #Either str or bytes depending on the output mode.
  167. if isinstance(stream, io.StringIO):
  168. output = output.encode("utf-8")
  169. parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
  170. self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
  171. @pyqtProperty(QObject, notify=activePrinterChanged)
  172. def activePrinter(self) -> Optional[PrinterOutputModel]:
  173. return self._active_printer
  174. @pyqtSlot(QObject)
  175. def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
  176. if self._active_printer != printer:
  177. if self._active_printer and self._active_printer.camera:
  178. self._active_printer.camera.stop()
  179. self._active_printer = printer
  180. self.activePrinterChanged.emit()
  181. def _onPostPrintJobFinished(self, reply):
  182. self._progress_message.hide()
  183. self._compressing_gcode = False
  184. self._sending_gcode = False
  185. def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
  186. if bytes_total > 0:
  187. new_progress = bytes_sent / bytes_total * 100
  188. # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
  189. # timeout responses if this happens.
  190. self._last_response_time = time()
  191. if new_progress > self._progress_message.getProgress():
  192. self._progress_message.show() # Ensure that the message is visible.
  193. self._progress_message.setProgress(bytes_sent / bytes_total * 100)
  194. # If successfully sent:
  195. if bytes_sent == bytes_total:
  196. # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
  197. # monitor tab.
  198. self._success_message = Message(
  199. i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
  200. lifetime=5, dismissable=True,
  201. title=i18n_catalog.i18nc("@info:title", "Data Sent"))
  202. self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None,
  203. description="")
  204. self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
  205. self._success_message.show()
  206. else:
  207. self._progress_message.setProgress(0)
  208. self._progress_message.hide()
  209. def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
  210. if action_id == "Abort":
  211. Logger.log("d", "User aborted sending print to remote.")
  212. self._progress_message.hide()
  213. self._compressing_gcode = False
  214. self._sending_gcode = False
  215. Application.getInstance().getController().setActiveStage("PrepareStage")
  216. # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
  217. # the "reply" should be disconnected
  218. if self._latest_reply_handler:
  219. self._latest_reply_handler.disconnect()
  220. self._latest_reply_handler = None
  221. def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
  222. if action_id == "View":
  223. Application.getInstance().getController().setActiveStage("MonitorStage")
  224. @pyqtSlot()
  225. def openPrintJobControlPanel(self) -> None:
  226. Logger.log("d", "Opening print job control panel...")
  227. QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
  228. @pyqtSlot()
  229. def openPrinterControlPanel(self) -> None:
  230. Logger.log("d", "Opening printer control panel...")
  231. QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
  232. @pyqtProperty("QVariantList", notify=printJobsChanged)
  233. def printJobs(self)-> List[PrintJobOutputModel] :
  234. return self._print_jobs
  235. @pyqtProperty("QVariantList", notify=printJobsChanged)
  236. def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
  237. return [print_job for print_job in self._print_jobs if print_job.state == "queued"]
  238. @pyqtProperty("QVariantList", notify=printJobsChanged)
  239. def activePrintJobs(self) -> List[PrintJobOutputModel]:
  240. return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
  241. @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
  242. def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
  243. printer_count = {}
  244. for printer in self._printers:
  245. if printer.type in printer_count:
  246. printer_count[printer.type] += 1
  247. else:
  248. printer_count[printer.type] = 1
  249. result = []
  250. for machine_type in printer_count:
  251. result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
  252. return result
  253. @pyqtSlot(int, result=str)
  254. def formatDuration(self, seconds: int) -> str:
  255. return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
  256. @pyqtSlot(int, result=str)
  257. def getTimeCompleted(self, time_remaining: int) -> str:
  258. current_time = time()
  259. datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
  260. return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
  261. @pyqtSlot(int, result=str)
  262. def getDateCompleted(self, time_remaining: int) -> str:
  263. current_time = time()
  264. datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
  265. return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
  266. def _printJobStateChanged(self) -> None:
  267. username = self._getUserName()
  268. if username is None:
  269. return # We only want to show notifications if username is set.
  270. finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
  271. newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
  272. for job in newly_finished_jobs:
  273. if job.assignedPrinter:
  274. job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
  275. else:
  276. job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
  277. job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
  278. job_completed_message.show()
  279. # Ensure UI gets updated
  280. self.printJobsChanged.emit()
  281. # Keep a list of all completed jobs so we know if something changed next time.
  282. self._finished_jobs = finished_jobs
  283. def _update(self) -> None:
  284. if not super()._update():
  285. return
  286. self.get("printers/", onFinished=self._onGetPrintersDataFinished)
  287. self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
  288. def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
  289. if not checkValidGetReply(reply):
  290. return
  291. result = loadJsonFromReply(reply)
  292. if result is None:
  293. return
  294. print_jobs_seen = []
  295. job_list_changed = False
  296. for print_job_data in result:
  297. print_job = findByKey(self._print_jobs, print_job_data["uuid"])
  298. if print_job is None:
  299. print_job = self._createPrintJobModel(print_job_data)
  300. job_list_changed = True
  301. self._updatePrintJob(print_job, print_job_data)
  302. if print_job.state != "queued": # Print job should be assigned to a printer.
  303. if print_job.state in ["failed", "finished", "aborted"]:
  304. # Print job was already completed, so don't attach it to a printer.
  305. printer = None
  306. else:
  307. printer = self._getPrinterByKey(print_job_data["printer_uuid"])
  308. else: # The job can "reserve" a printer if some changes are required.
  309. printer = self._getPrinterByKey(print_job_data["assigned_to"])
  310. if printer:
  311. printer.updateActivePrintJob(print_job)
  312. print_jobs_seen.append(print_job)
  313. # Check what jobs need to be removed.
  314. removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
  315. for removed_job in removed_jobs:
  316. job_list_changed |= self._removeJob(removed_job)
  317. if job_list_changed:
  318. self.printJobsChanged.emit() # Do a single emit for all print job changes.
  319. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
  320. if not checkValidGetReply(reply):
  321. return
  322. result = loadJsonFromReply(reply)
  323. if result is None:
  324. return
  325. printer_list_changed = False
  326. printers_seen = []
  327. for printer_data in result:
  328. printer = findByKey(self._printers, printer_data["uuid"])
  329. if printer is None:
  330. printer = self._createPrinterModel(printer_data)
  331. printer_list_changed = True
  332. printers_seen.append(printer)
  333. self._updatePrinter(printer, printer_data)
  334. removed_printers = [printer for printer in self._printers if printer not in printers_seen]
  335. for printer in removed_printers:
  336. self._removePrinter(printer)
  337. if removed_printers or printer_list_changed:
  338. self.printersChanged.emit()
  339. def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
  340. printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
  341. number_of_extruders=self._number_of_extruders)
  342. printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
  343. self._printers.append(printer)
  344. return printer
  345. def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel:
  346. print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
  347. key=data["uuid"], name= data["name"])
  348. print_job.stateChanged.connect(self._printJobStateChanged)
  349. self._print_jobs.append(print_job)
  350. return print_job
  351. def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None:
  352. print_job.updateTimeTotal(data["time_total"])
  353. print_job.updateTimeElapsed(data["time_elapsed"])
  354. print_job.updateState(data["status"])
  355. print_job.updateOwner(data["owner"])
  356. def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None:
  357. # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
  358. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
  359. self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
  360. definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
  361. if not definitions:
  362. Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
  363. return
  364. machine_definition = definitions[0]
  365. printer.updateName(data["friendly_name"])
  366. printer.updateKey(data["uuid"])
  367. printer.updateType(data["machine_variant"])
  368. # Do not store the buildplate information that comes from connect if the current printer has not buildplate information
  369. if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
  370. printer.updateBuildplateName(data["build_plate"]["type"])
  371. if not data["enabled"]:
  372. printer.updateState("disabled")
  373. else:
  374. printer.updateState(data["status"])
  375. for index in range(0, self._number_of_extruders):
  376. extruder = printer.extruders[index]
  377. try:
  378. extruder_data = data["configuration"][index]
  379. except IndexError:
  380. break
  381. extruder.updateHotendID(extruder_data.get("print_core_id", ""))
  382. material_data = extruder_data["material"]
  383. if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
  384. containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
  385. GUID=material_data["guid"])
  386. if containers:
  387. color = containers[0].getMetaDataEntry("color_code")
  388. brand = containers[0].getMetaDataEntry("brand")
  389. material_type = containers[0].getMetaDataEntry("material")
  390. name = containers[0].getName()
  391. else:
  392. Logger.log("w",
  393. "Unable to find material with guid {guid}. Using data as provided by cluster".format(
  394. guid=material_data["guid"]))
  395. color = material_data["color"]
  396. brand = material_data["brand"]
  397. material_type = material_data["material"]
  398. name = "Empty" if material_data["material"] == "empty" else "Unknown"
  399. material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
  400. brand=brand, color=color, name=name)
  401. extruder.updateActiveMaterial(material)
  402. def _removeJob(self, job: PrintJobOutputModel):
  403. if job not in self._print_jobs:
  404. return False
  405. if job.assignedPrinter:
  406. job.assignedPrinter.updateActivePrintJob(None)
  407. job.stateChanged.disconnect(self._printJobStateChanged)
  408. self._print_jobs.remove(job)
  409. return True
  410. def _removePrinter(self, printer: PrinterOutputModel):
  411. self._printers.remove(printer)
  412. if self._active_printer == printer:
  413. self._active_printer = None
  414. self.activePrinterChanged.emit()
  415. def loadJsonFromReply(reply):
  416. try:
  417. result = json.loads(bytes(reply.readAll()).decode("utf-8"))
  418. except json.decoder.JSONDecodeError:
  419. Logger.logException("w", "Unable to decode JSON from reply.")
  420. return
  421. return result
  422. def checkValidGetReply(reply):
  423. status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
  424. if status_code != 200:
  425. Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
  426. return False
  427. return True
  428. def findByKey(list, key):
  429. for item in list:
  430. if item.key == key:
  431. return item