diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 300ed5194d..b0c8b54a67 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -4,6 +4,7 @@ from UM.FileHandler.FileHandler import FileHandler #For typing. from UM.Logger import Logger from UM.Scene.SceneNode import SceneNode #For typing. +from cura.API import Account from cura.CuraApplication import CuraApplication from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState @@ -162,9 +163,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): part.setBody(data) return part - ## Convenience function to get the username from the OS. - # The code was copied from the getpass module, as we try to use as little dependencies as possible. + ## Convenience function to get the username, either from the cloud or from the OS. def _getUserName(self) -> str: + # check first if we are logged in with the Ultimaker Account + account = CuraApplication.getInstance().getCuraAPI().account # type: Account + if account and account.isLoggedIn: + return account.userName + + # Otherwise get the username from the US + # The code below was copied from the getpass module, as we try to use as little dependencies as possible. for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): user = os.environ.get(name) if user: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 9f5857dff6..3bc16cbfb0 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -16,7 +16,6 @@ from UM.Qt.Duration import Duration, DurationFormat from UM.Scene.SceneNode import SceneNode from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice -from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController from ..MeshFormatHandler import MeshFormatHandler @@ -28,7 +27,7 @@ from .Models.CloudPrintResponse import CloudPrintResponse from .Models.CloudJobResponse import CloudJobResponse from .Models.CloudClusterPrinter import CloudClusterPrinter from .Models.CloudClusterPrintJob import CloudClusterPrintJob -from .Utils import findChanges +from .Utils import findChanges, formatDateCompleted, formatTimeCompleted ## Class that contains all the translations for this module. @@ -55,6 +54,12 @@ class T: UPLOAD_SUCCESS_TITLE = _I18N_CATALOG.i18nc("@info:title", "Data Sent") UPLOAD_SUCCESS_TEXT = _I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer.") + JOB_COMPLETED_TITLE = _I18N_CATALOG.i18nc("@info:status", "Print finished") + JOB_COMPLETED_PRINTER = _I18N_CATALOG.i18nc("@info:status", + "Printer '{printer_name}' has finished printing '{job_name}'.") + + JOB_COMPLETED_NO_PRINTER = _I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.") + ## The cloud output device is a network output device that works remotely but has limited functionality. # Currently it only supports viewing the printer and print job status and adding a new job to the queue. @@ -65,7 +70,7 @@ class T: class CloudOutputDevice(NetworkedPrinterOutputDevice): # The interval with which the remote clusters are checked - CHECK_CLUSTER_INTERVAL = 2.0 # seconds + CHECK_CLUSTER_INTERVAL = 4.0 # seconds # Signal triggered when the print jobs in the queue were changed. printJobsChanged = pyqtSignal() @@ -109,6 +114,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # We only allow a single upload at a time. self._sending_job = False + # TODO: handle progress messages in another class. self._progress_message = None # type: Optional[Message] ## Gets the host name of this device @@ -128,7 +134,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): return network_key.startswith(self._host_name) ## Set all the interface elements and texts for this output device. - def _setInterfaceElements(self): + def _setInterfaceElements(self) -> None: self.setPriority(2) # make sure we end up below the local networking and above 'save to file' self.setName(self._id) self.setShortDescription(T.PRINT_VIA_CLOUD_BUTTON) @@ -157,13 +163,192 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # TODO: Remove extension from the file name, since we are using content types now request = CloudJobUploadRequest( - job_name = file_name + "." + mesh_format.file_extension, + job_name = file_name, ## + "." + mesh_format.file_extension, file_size = len(mesh_bytes), content_type = mesh_format.mime_type, ) self._api.requestUpload(request, lambda response: self._onPrintJobCreated(mesh_bytes, response)) - ## Get remote printers. + ## Called when the connection to the cluster changes. + def connect(self) -> None: + super().connect() + + ## Called when the network data should be updated. + def _update(self) -> None: + super()._update() + if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL: + return # avoid calling the cloud too often + + if self._account.isLoggedIn: + self.setAuthenticationState(AuthState.Authenticated) + self._api.getClusterStatus(self._device_id, self._onStatusCallFinished) + else: + self.setAuthenticationState(AuthState.NotAuthenticated) + + ## Method called when HTTP request to status endpoint is finished. + # Contains both printers and print jobs statuses in a single response. + def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: + # Update all data from the cluster. + self._updatePrinters(status.printers) + self._updatePrintJobs(status.print_jobs) + + ## Updates the local list of printers with the list received from the cloud. + # \param jobs: The printers received from the cloud. + def _updatePrinters(self, printers: List[CloudClusterPrinter]) -> None: + previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel] + received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinter] + + removed_printers, added_printers, updated_printers = findChanges(previous, received) + + for removed_printer in removed_printers: + if self._active_printer == removed_printer: + self.setActivePrinter(None) + self._printers.remove(removed_printer) + + for added_printer in added_printers: + self._printers.append(added_printer.createOutputModel(CloudOutputController(self))) + + for model, printer in updated_printers: + printer.updateOutputModel(model) + + # Always have an active printer + if not self._active_printer: + self.setActivePrinter(self._printers[0]) + + if removed_printers or added_printers or updated_printers: + self._clusterPrintersChanged.emit() + + ## Updates the local list of print jobs with the list received from the cloud. + # \param jobs: The print jobs received from the cloud. + def _updatePrintJobs(self, jobs: List[CloudClusterPrintJob]) -> None: + received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJob] + previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel] + + removed_jobs, added_jobs, updated_jobs = findChanges(previous, received) + + # TODO: we see that not all data in the UI is correctly updated when the queue and active jobs change. + # TODO: we need to fix this here somehow by updating the correct output models. + # TODO: the configuration drop down in the slice window is not populated because we are missing some data. + # TODO: to fix this we need to implement more data as shown in ClusterUM3OutputDevice._createPrintJobModel + + for removed_job in removed_jobs: + self._print_jobs.remove(removed_job) + + for added_job in added_jobs: + self._addPrintJob(added_job) + + for model, job in updated_jobs: + job.updateOutputModel(model) + if job.printer_uuid: + self._updateAssignedPrinter(model, job.printer_uuid) + + # We only have to update when jobs are added or removed + # updated jobs push their changes via their output model + if added_jobs or removed_jobs or updated_jobs: + self.printJobsChanged.emit() + + ## Registers a new print job received via the cloud API. + # \param job: The print job received. + def _addPrintJob(self, job: CloudClusterPrintJob) -> None: + model = job.createOutputModel(CloudOutputController(self)) + model.stateChanged.connect(self._onPrintJobStateChanged) + if job.printer_uuid: + self._updateAssignedPrinter(model, job.printer_uuid) + self._print_jobs.append(model) + + ## Handles the event of a change in a print job state + def _onPrintJobStateChanged(self) -> None: + username = self._account.userName + finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"] + + newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username] + for job in newly_finished_jobs: + if job.assignedPrinter: + job_completed_text = T.JOB_COMPLETED_PRINTER.format(printer_name=job.assignedPrinter.name, + job_name=job.name) + else: + job_completed_text = T.JOB_COMPLETED_NO_PRINTER.format(job_name=job.name) + job_completed_message = Message(text=job_completed_text, title = T.JOB_COMPLETED_TITLE) + job_completed_message.show() + + # Ensure UI gets updated + self.printJobsChanged.emit() + + ## Updates the printer assignment for the given print job model. + def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: + printer = next((p for p in self._printers if printer_uuid == p.key), None) + + if not printer: + return Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key, + [p.key for p in self._printers]) + + printer.updateActivePrintJob(model) + model.updateAssignedPrinter(printer) + + ## Uploads the mesh when the print job was registered with the cloud API. + # \param mesh: The bytes to upload. + # \param job_response: The response received from the cloud API. + def _onPrintJobCreated(self, mesh: bytes, job_response: CloudJobResponse) -> None: + self._api.uploadMesh(job_response, mesh, self._onPrintJobUploaded, self._updateUploadProgress, + lambda _: self._onUploadError(T.UPLOAD_ERROR)) + + ## Requests the print to be sent to the printer when we finished uploading the mesh. + # \param job_id: The ID of the job. + def _onPrintJobUploaded(self, job_id: str) -> None: + self._api.requestPrint(self._device_id, job_id, self._onUploadSuccess) + + ## Updates the progress of the mesh upload. + # \param progress: The amount of percentage points uploaded until now (0-100). + def _updateUploadProgress(self, progress: int) -> None: + if not self._progress_message: + self._progress_message = Message( + text = T.SENDING_DATA_TEXT, + title = T.SENDING_DATA_TITLE, + progress = -1, + lifetime = 0, + dismissable = False, + use_inactivity_timer = False + ) + self._progress_message.setProgress(progress) + self._progress_message.show() + + ## Hides the upload progress bar + def _resetUploadProgress(self) -> None: + if self._progress_message: + self._progress_message.hide() + self._progress_message = None + + ## Displays the given message if uploading the mesh has failed + # \param message: The message to display. + def _onUploadError(self, message: str = None) -> None: + self._resetUploadProgress() + if message: + message = Message( + text = message, + title = T.ERROR, + lifetime = 10, + dismissable = True + ) + message.show() + self._sending_job = False # the upload has finished so we're not sending a job anymore + self.writeError.emit() + + ## Shows a message when the upload has succeeded + # \param response: The response from the cloud API. + def _onUploadSuccess(self, response: CloudPrintResponse) -> None: + Logger.log("i", "The cluster will be printing this print job with the ID %s", response.cluster_job_id) + self._resetUploadProgress() + message = Message( + text = T.UPLOAD_SUCCESS_TEXT, + title = T.UPLOAD_SUCCESS_TITLE, + lifetime = 5, + dismissable = True, + ) + message.show() + self._sending_job = False # the upload has finished so we're not sending a job anymore + self.writeFinished.emit() + + ## Gets the remote printers. @pyqtProperty("QVariantList", notify = _clusterPrintersChanged) def printers(self) -> List[PrinterOutputModel]: return self._printers @@ -209,170 +394,11 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): @pyqtSlot(int, result = str) def getTimeCompleted(self, time_remaining: int) -> str: # TODO: this really shouldn't be in this class - current_time = time() - datetime_completed = datetime.fromtimestamp(current_time + time_remaining) - return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute) + return formatTimeCompleted(time_remaining) @pyqtSlot(int, result = str) def getDateCompleted(self, time_remaining: int) -> str: - # TODO: this really shouldn't be in this class - current_time = time() - completed = datetime.fromtimestamp(current_time + time_remaining) - today = datetime.fromtimestamp(current_time) - # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format - if completed.toordinal() > today.toordinal() + 7: - return completed.strftime("%a %b ") + "{day}".format(day = completed.day) - # If finishing date is within the next week, use "Monday at HH:MM" format - elif completed.toordinal() > today.toordinal() + 1: - return completed.strftime("%a") - # If finishing tomorrow, use "tomorrow at HH:MM" format - elif completed.toordinal() > today.toordinal(): - return "tomorrow" - # If finishing today, use "today at HH:MM" format - else: - return "today" - - ## Called when the connection to the cluster changes. - def connect(self) -> None: - super().connect() - - ## Called when the network data should be updated. - def _update(self) -> None: - super()._update() - if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL: - return # avoid calling the cloud too often - - if self._account.isLoggedIn: - self.setAuthenticationState(AuthState.Authenticated) - self._api.getClusterStatus(self._device_id, self._onStatusCallFinished) - else: - self.setAuthenticationState(AuthState.NotAuthenticated) - - ## Method called when HTTP request to status endpoint is finished. - # Contains both printers and print jobs statuses in a single response. - def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: - # Update all data from the cluster. - self._updatePrinters(status.printers) - self._updatePrintJobs(status.print_jobs) - - def _updatePrinters(self, printers: List[CloudClusterPrinter]) -> None: - previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel] - received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinter] - - removed_printers, added_printers, updated_printers = findChanges(previous, received) - - for removed_printer in removed_printers: - if self._active_printer == removed_printer: - self.setActivePrinter(None) - self._printers.remove(removed_printer) - - for added_printer in added_printers: - self._printers.append(added_printer.createOutputModel(CloudOutputController(self))) - - for model, printer in updated_printers: - printer.updateOutputModel(model) - - # Always have an active printer - if not self._active_printer: - self.setActivePrinter(self._printers[0]) - - if removed_printers or added_printers or updated_printers: - self._clusterPrintersChanged.emit() - - def _updatePrintJobs(self, jobs: List[CloudClusterPrintJob]) -> None: - received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJob] - previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel] - - removed_jobs, added_jobs, updated_jobs = findChanges(previous, received) - - # TODO: we see that not all data in the UI is correctly updated when the queue and active jobs change. - # TODO: we need to fix this here somehow by updating the correct output models. - # TODO: also the configuration drop down in the slice window is not populated because we are missing some data. - # TODO: to fix this we need to implement more data as shown in ClusterUM3OutputDevice._createPrintJobModel - - for removed_job in removed_jobs: - self._print_jobs.remove(removed_job) - - for added_job in added_jobs: - self._addPrintJob(added_job) - - for model, job in updated_jobs: - job.updateOutputModel(model) - self._updatePrintJobDetails(model) - - # We only have to update when jobs are added or removed - # updated jobs push their changes via their output model - if added_jobs or removed_jobs or updated_jobs: - self.printJobsChanged.emit() - - def _addPrintJob(self, job: CloudClusterPrintJob) -> None: - print_job = job.createOutputModel(CloudOutputController(self)) - self._updatePrintJobDetails(print_job) - self._print_jobs.append(print_job) - - def _updatePrintJobDetails(self, print_job: UM3PrintJobOutputModel): - printer = None - try: - printer = next(p for p in self._printers if print_job.assignedPrinter == p.key) - except StopIteration: - Logger.log("w", "Missing printer %s for job %s in %s", print_job.assignedPrinter, print_job.key, - [p.key for p in self._printers]) - - if printer: - printer.updateActivePrintJob(print_job) - print_job.updateAssignedPrinter(printer) - - def _onPrintJobCreated(self, mesh: bytes, job_response: CloudJobResponse) -> None: - self._api.uploadMesh(job_response, mesh, self._onPrintJobUploaded, self._updateUploadProgress, - lambda _: self._onUploadError(T.UPLOAD_ERROR)) - - def _onPrintJobUploaded(self, job_id: str) -> None: - self._api.requestPrint(self._device_id, job_id, self._onUploadSuccess) - - def _updateUploadProgress(self, progress: int): - if not self._progress_message: - self._progress_message = Message( - text = T.SENDING_DATA_TEXT, - title = T.SENDING_DATA_TITLE, - progress = -1, - lifetime = 0, - dismissable = False, - use_inactivity_timer = False - ) - self._progress_message.setProgress(progress) - self._progress_message.show() - - def _resetUploadProgress(self): - if self._progress_message: - self._progress_message.hide() - self._progress_message = None - - def _onUploadError(self, message: str = None): - self._resetUploadProgress() - if message: - message = Message( - text = message, - title = T.ERROR, - lifetime = 10, - dismissable = True - ) - message.show() - self._sending_job = False # the upload has finished so we're not sending a job anymore - self.writeError.emit() - - # Shows a message when the upload has succeeded - def _onUploadSuccess(self, response: CloudPrintResponse): - Logger.log("i", "The cluster will be printing this print job with the ID %s", response.cluster_job_id) - self._resetUploadProgress() - message = Message( - text = T.UPLOAD_SUCCESS_TEXT, - title = T.UPLOAD_SUCCESS_TITLE, - lifetime = 5, - dismissable = True, - ) - message.show() - self._sending_job = False # the upload has finished so we're not sending a job anymore - self.writeFinished.emit() + return formatDateCompleted(time_remaining) ## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud. # TODO: We fake the methods here to not break the monitor page. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJob.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJob.py index c9255b8da8..22c66ddfab 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJob.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJob.py @@ -1,7 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import List, Optional +from typing import List +from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration from .CloudClusterPrintJobConstraint import CloudClusterPrintJobConstraint @@ -31,20 +32,33 @@ class CloudClusterPrintJob(BaseModel): self.time_total = None # type: str self.uuid = None # type: str super().__init__(**kwargs) - self.printers = [CloudClusterPrinterConfiguration(**c) if isinstance(c, dict) else c - for c in self.configuration] - self.print_jobs = [CloudClusterPrintJobConstraint(**p) if isinstance(p, dict) else p - for p in self.constraints] + self.configuration = [CloudClusterPrinterConfiguration(**c) if isinstance(c, dict) else c + for c in self.configuration] + self.constraints = [CloudClusterPrintJobConstraint(**p) if isinstance(p, dict) else p + for p in self.constraints] ## Creates an UM3 print job output model based on this cloud cluster print job. # \param printer: The output model of the printer def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel: model = UM3PrintJobOutputModel(controller, self.uuid, self.name) + self.updateOutputModel(model) + return model + ## Creates a new configuration model + def _createConfigurationModel(self) -> ConfigurationModel: + extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()] + configuration = ConfigurationModel() + configuration.setExtruderConfigurations(extruders) + return configuration + ## Updates an UM3 print job output model based on this cloud cluster print job. # \param model: The model to update. def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None: + # TODO: Add `compatible_machine_families` to the cloud, than add model.setCompatibleMachineFamilies() + # TODO: Add `impediments_to_printing` to the cloud, see ClusterUM3OutputDevice._updatePrintJob + # TODO: Use model.updateConfigurationChanges, see ClusterUM3OutputDevice#_createConfigurationChanges + model.updateConfiguration(self._createConfigurationModel()) model.updateTimeTotal(self.time_total) model.updateTimeElapsed(self.time_elapsed) model.updateOwner(self.owner) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinter.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinter.py index dd65dffa26..78aa8e3a31 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinter.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinter.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import List +from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration @@ -40,5 +41,9 @@ class CloudClusterPrinter(BaseModel): model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") - for configuration, extruder in zip(self.configuration, model.extruders): - configuration.updateOutputModel(extruder) + for configuration, extruder_output, extruder_config in \ + zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): + configuration.updateOutputModel(extruder_output) + configuration.updateConfigurationModel(extruder_config) + + pass diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfiguration.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfiguration.py index be92549015..d60395f6ab 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfiguration.py @@ -1,5 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List, Optional + +from cura.PrinterOutput.ConfigurationModel import ConfigurationModel +from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial from ...Models import BaseModel @@ -8,7 +12,7 @@ from ...Models import BaseModel ## Class representing a cloud cluster printer configuration class CloudClusterPrinterConfiguration(BaseModel): def __init__(self, **kwargs) -> None: - self.extruder_index = None # type: str + self.extruder_index = None # type: int self.material = None # type: CloudClusterPrinterConfigurationMaterial self.nozzle_diameter = None # type: str self.print_core_id = None # type: str @@ -25,3 +29,16 @@ class CloudClusterPrinterConfiguration(BaseModel): if model.activeMaterial is None or model.activeMaterial.guid != self.material.guid: material = self.material.createOutputModel() model.updateActiveMaterial(material) + + ## Creates a configuration model + def createConfigurationModel(self) -> ExtruderConfigurationModel: + model = ExtruderConfigurationModel(position = self.extruder_index) + self.updateConfigurationModel(model) + return model + + ## Creates a configuration model + def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel: + model.setHotendID(self.print_core_id) + if self.material: + model.setMaterial(self.material.createOutputModel()) + return model diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Utils.py b/plugins/UM3NetworkPrinting/src/Cloud/Utils.py index 58eaf5edb9..eb96e49dad 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Utils.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Utils.py @@ -1,5 +1,8 @@ +from datetime import datetime, timedelta from typing import TypeVar, Dict, Tuple, List +from UM import i18nCatalog + T = TypeVar("T") U = TypeVar("U") @@ -24,3 +27,27 @@ def findChanges(previous: Dict[str, T], received: Dict[str, U]) -> Tuple[List[T] updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids] return removed, added, updated + + +def formatTimeCompleted(time_remaining: int) -> str: + completed = datetime.now() + timedelta(seconds=time_remaining) + return "{hour:02d}:{minute:02d}".format(hour = completed.hour, minute = completed.minute) + + +def formatDateCompleted(time_remaining: int) -> str: + remaining = timedelta(seconds=time_remaining) + completed = datetime.now() + remaining + i18n = i18nCatalog("cura") + + # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format + if remaining.days >= 7: + return completed.strftime("%a %b ") + "{day}".format(day = completed.day) + # If finishing date is within the next week, use "Monday at HH:MM" format + elif remaining.days >= 2: + return completed.strftime("%a") + # If finishing tomorrow, use "tomorrow at HH:MM" format + elif remaining.days >= 1: + return i18n.i18nc("@info:status", "tomorrow") + # If finishing today, use "today at HH:MM" format + else: + return i18n.i18nc("@info:status", "today") diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index a5ee3bc650..93a53373dc 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -25,6 +25,7 @@ from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationM from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from .ConfigurationChangeModel import ConfigurationChangeModel @@ -337,14 +338,12 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return self._printers @pyqtSlot(int, result = str) - def formatDuration(self, seconds: int) -> str: - return Duration(seconds).getDisplayString(DurationFormat.Format.Short) + def getTimeCompleted(self, time_remaining: int) -> str: + return formatTimeCompleted(time_remaining) @pyqtSlot(int, result = str) - def getTimeCompleted(self, time_remaining: int) -> str: - current_time = time() - datetime_completed = datetime.fromtimestamp(current_time + time_remaining) - return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute) + def getDateCompleted(self, time_remaining: int) -> str: + return formatDateCompleted(time_remaining) @pyqtSlot(int, result = str) def getDateCompleted(self, time_remaining: int) -> str: