Merge pull request #20680 from Ultimaker/CURA-12557_handle-deactivated-things
Some checks failed
conan-package / conan-package (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
unit-test / Run unit tests (push) Has been cancelled

CURA-12557 handle deactivated things
This commit is contained in:
HellAholic 2025-06-30 15:00:07 +02:00 committed by GitHub
commit 4e04d06db0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 207 additions and 48 deletions

View file

@ -33,8 +33,8 @@ class AuthState(IntEnum):
class NetworkedPrinterOutputDevice(PrinterOutputDevice): class NetworkedPrinterOutputDevice(PrinterOutputDevice):
authenticationStateChanged = pyqtSignal() authenticationStateChanged = pyqtSignal()
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None: def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None, active: bool = True) -> None:
super().__init__(device_id = device_id, connection_type = connection_type, parent = parent) super().__init__(device_id = device_id, connection_type = connection_type, parent = parent, active = active)
self._manager = None # type: Optional[QNetworkAccessManager] self._manager = None # type: Optional[QNetworkAccessManager]
self._timeout_time = 10 # After how many seconds of no response should a timeout occur? self._timeout_time = 10 # After how many seconds of no response should a timeout occur?

View file

@ -72,7 +72,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Signal to indicate that the configuration of one of the printers has changed. # Signal to indicate that the configuration of one of the printers has changed.
uniqueConfigurationsChanged = pyqtSignal() uniqueConfigurationsChanged = pyqtSignal()
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None: # Signal to indicate that the printer has become active or inactive
activeChanged = pyqtSignal()
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None, active: bool = True) -> None:
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
self._printers = [] # type: List[PrinterOutputModel] self._printers = [] # type: List[PrinterOutputModel]
@ -88,6 +91,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._accepts_commands = False # type: bool self._accepts_commands = False # type: bool
self._active: bool = active
self._update_timer = QTimer() # type: QTimer self._update_timer = QTimer() # type: QTimer
self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setInterval(2000) # TODO; Add preference for update interval
self._update_timer.setSingleShot(False) self._update_timer.setSingleShot(False)
@ -295,3 +300,17 @@ class PrinterOutputDevice(QObject, OutputDevice):
return return
self._firmware_updater.updateFirmware(firmware_file) self._firmware_updater.updateFirmware(firmware_file)
@pyqtProperty(bool, notify = activeChanged)
def active(self) -> bool:
"""
Indicates whether the printer is active, which is not the same as "being the active printer". In this case,
active means that the printer can be used. An example of an inactive printer is one that cannot be used because
the user doesn't have enough seats on Digital Factory.
"""
return self._active
def _setActive(self, active: bool) -> None:
if active != self._active:
self._active = active
self.activeChanged.emit()

View file

@ -183,10 +183,14 @@ class MachineManager(QObject):
self.setActiveMachine(active_machine_id) self.setActiveMachine(active_machine_id)
def _onOutputDevicesChanged(self) -> None: def _onOutputDevicesChanged(self) -> None:
for printer_output_device in self._printer_output_devices:
printer_output_device.activeChanged.disconnect(self.printerConnectedStatusChanged)
self._printer_output_devices = [] self._printer_output_devices = []
for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices():
if isinstance(printer_output_device, PrinterOutputDevice): if isinstance(printer_output_device, PrinterOutputDevice):
self._printer_output_devices.append(printer_output_device) self._printer_output_devices.append(printer_output_device)
printer_output_device.activeChanged.connect(self.printerConnectedStatusChanged)
self.outputDevicesChanged.emit() self.outputDevicesChanged.emit()
@ -569,6 +573,13 @@ class MachineManager(QObject):
def activeMachineIsUsingCloudConnection(self) -> bool: def activeMachineIsUsingCloudConnection(self) -> bool:
return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsActive(self) -> bool:
if not self._printer_output_devices:
return True
return self._printer_output_devices[0].active
def activeMachineNetworkKey(self) -> str: def activeMachineNetworkKey(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.getMetaDataEntry("um_network_key", "") return self._global_container_stack.getMetaDataEntry("um_network_key", "")

View file

@ -11,10 +11,10 @@ Cura.RoundedRectangle
width: parent.width width: parent.width
height: projectImage.height + 2 * UM.Theme.getSize("default_margin").width height: projectImage.height + 2 * UM.Theme.getSize("default_margin").width
cornerSide: Cura.RoundedRectangle.Direction.All cornerSide: Cura.RoundedRectangle.Direction.All
border.color: UM.Theme.getColor("lining") border.color: enabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("action_button_disabled_border")
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
radius: UM.Theme.getSize("default_radius").width radius: UM.Theme.getSize("default_radius").width
color: UM.Theme.getColor("main_background") color: getBackgroundColor()
signal clicked() signal clicked()
property alias imageSource: projectImage.source property alias imageSource: projectImage.source
property alias projectNameText: displayNameLabel.text property alias projectNameText: displayNameLabel.text
@ -22,17 +22,18 @@ Cura.RoundedRectangle
property alias projectLastUpdatedText: lastUpdatedLabel.text property alias projectLastUpdatedText: lastUpdatedLabel.text
property alias cardMouseAreaEnabled: cardMouseArea.enabled property alias cardMouseAreaEnabled: cardMouseArea.enabled
onVisibleChanged: color = UM.Theme.getColor("main_background") onVisibleChanged: color = getBackgroundColor()
MouseArea MouseArea
{ {
id: cardMouseArea id: cardMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: base.enabled
onEntered: base.color = UM.Theme.getColor("action_button_hovered") onEntered: color = getBackgroundColor()
onExited: base.color = UM.Theme.getColor("main_background") onExited: color = getBackgroundColor()
onClicked: base.clicked() onClicked: base.clicked()
} }
Row Row
{ {
id: projectInformationRow id: projectInformationRow
@ -73,7 +74,7 @@ Cura.RoundedRectangle
width: parent.width width: parent.width
height: Math.round(parent.height / 3) height: Math.round(parent.height / 3)
elide: Text.ElideRight elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text") color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled")
} }
UM.Label UM.Label
@ -82,8 +83,27 @@ Cura.RoundedRectangle
width: parent.width width: parent.width
height: Math.round(parent.height / 3) height: Math.round(parent.height / 3)
elide: Text.ElideRight elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text") color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled")
} }
} }
} }
}
function getBackgroundColor()
{
if(enabled)
{
if(cardMouseArea.containsMouse)
{
return UM.Theme.getColor("action_button_hovered")
}
else
{
return UM.Theme.getColor("main_background")
}
}
else
{
return UM.Theme.getColor("action_button_disabled")
}
}
}

View file

@ -159,17 +159,30 @@ Item
Repeater Repeater
{ {
model: manager.digitalFactoryProjectModel model: manager.digitalFactoryProjectModel
delegate: ProjectSummaryCard delegate: Item
{ {
id: projectSummaryCard width: parent.width
imageSource: model.thumbnailUrl || "../images/placeholder.svg" height: projectSummaryCard.height
projectNameText: model.displayName
projectUsernameText: model.username
projectLastUpdatedText: "Last updated: " + model.lastUpdated
onClicked: UM.TooltipArea
{ {
manager.selectedProjectIndex = index anchors.fill: parent
text: "This project is inactive and cannot be used."
enabled: !model.active
}
ProjectSummaryCard
{
id: projectSummaryCard
imageSource: model.thumbnailUrl || "../images/placeholder.svg"
projectNameText: model.displayName
projectUsernameText: model.username
projectLastUpdatedText: "Last updated: " + model.lastUpdated
enabled: model.active
onClicked: {
manager.selectedProjectIndex = index
}
} }
} }
} }

View file

@ -17,6 +17,7 @@ class DigitalFactoryProjectModel(ListModel):
ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5 ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5
UsernameRole = Qt.ItemDataRole.UserRole + 6 UsernameRole = Qt.ItemDataRole.UserRole + 6
LastUpdatedRole = Qt.ItemDataRole.UserRole + 7 LastUpdatedRole = Qt.ItemDataRole.UserRole + 7
ActiveRole = Qt.ItemDataRole.UserRole + 8
dfProjectModelChanged = pyqtSignal() dfProjectModelChanged = pyqtSignal()
@ -28,6 +29,7 @@ class DigitalFactoryProjectModel(ListModel):
self.addRoleName(self.ThumbnailUrlRole, "thumbnailUrl") self.addRoleName(self.ThumbnailUrlRole, "thumbnailUrl")
self.addRoleName(self.UsernameRole, "username") self.addRoleName(self.UsernameRole, "username")
self.addRoleName(self.LastUpdatedRole, "lastUpdated") self.addRoleName(self.LastUpdatedRole, "lastUpdated")
self.addRoleName(self.ActiveRole, "active")
self._projects = [] # type: List[DigitalFactoryProjectResponse] self._projects = [] # type: List[DigitalFactoryProjectResponse]
def setProjects(self, df_projects: List[DigitalFactoryProjectResponse]) -> None: def setProjects(self, df_projects: List[DigitalFactoryProjectResponse]) -> None:
@ -59,5 +61,6 @@ class DigitalFactoryProjectModel(ListModel):
"thumbnailUrl": project.thumbnail_url, "thumbnailUrl": project.thumbnail_url,
"username": project.username, "username": project.username,
"lastUpdated": project.last_updated.strftime(PROJECT_UPDATED_AT_DATETIME_FORMAT) if project.last_updated else "", "lastUpdated": project.last_updated.strftime(PROJECT_UPDATED_AT_DATETIME_FORMAT) if project.last_updated else "",
"active": project.active,
}) })
self.dfProjectModelChanged.emit() self.dfProjectModelChanged.emit()

View file

@ -28,6 +28,7 @@ class DigitalFactoryProjectResponse(BaseModel):
team_ids: Optional[List[str]] = None, team_ids: Optional[List[str]] = None,
status: Optional[str] = None, status: Optional[str] = None,
technical_requirements: Optional[Dict[str, Any]] = None, technical_requirements: Optional[Dict[str, Any]] = None,
is_inactive: bool = False,
**kwargs) -> None: **kwargs) -> None:
""" """
Creates a new digital factory project response object Creates a new digital factory project response object
@ -56,6 +57,7 @@ class DigitalFactoryProjectResponse(BaseModel):
self.last_updated = datetime.strptime(last_updated, DIGITAL_FACTORY_RESPONSE_DATETIME_FORMAT) if last_updated else None self.last_updated = datetime.strptime(last_updated, DIGITAL_FACTORY_RESPONSE_DATETIME_FORMAT) if last_updated else None
self.status = status self.status = status
self.technical_requirements = technical_requirements self.technical_requirements = technical_requirements
self.active = not is_inactive
super().__init__(**kwargs) super().__init__(**kwargs)
def __str__(self) -> str: def __str__(self) -> str:

View file

@ -163,7 +163,7 @@ class CloudApiClient:
scope=self._scope, scope=self._scope,
data=b"", data=b"",
callback=self._parseCallback(on_finished, CloudPrintResponse), callback=self._parseCallback(on_finished, CloudPrintResponse),
error_callback=on_error, error_callback=self._parseError(on_error),
timeout=self.DEFAULT_REQUEST_TIMEOUT) timeout=self.DEFAULT_REQUEST_TIMEOUT)
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
@ -256,7 +256,6 @@ class CloudApiClient:
"""Creates a callback function so that it includes the parsing of the response into the correct model. """Creates a callback function so that it includes the parsing of the response into the correct model.
The callback is added to the 'finished' signal of the reply. The callback is added to the 'finished' signal of the reply.
:param reply: The reply that should be listened to.
:param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
a list or a single item. a list or a single item.
:param model: The type of the model to convert the response to. :param model: The type of the model to convert the response to.
@ -281,6 +280,25 @@ class CloudApiClient:
self._anti_gc_callbacks.append(parse) self._anti_gc_callbacks.append(parse)
return parse return parse
def _parseError(self,
on_error: Callable[[CloudError, "QNetworkReply.NetworkError", int], None]) -> Callable[[QNetworkReply, "QNetworkReply.NetworkError"], None]:
"""Creates a callback function so that it includes the parsing of an explicit error response into the correct model.
:param on_error: The callback in case the response gives an explicit error
"""
def parse(reply: QNetworkReply, error: "QNetworkReply.NetworkError") -> None:
self._anti_gc_callbacks.remove(parse)
http_code, response = self._parseReply(reply)
result = CloudError(**response["errors"][0])
on_error(result, error, http_code)
self._anti_gc_callbacks.append(parse)
return parse
@classmethod @classmethod
def getMachineIDMap(cls) -> Dict[str, str]: def getMachineIDMap(cls) -> Dict[str, str]:
if cls._machine_id_to_name is None: if cls._machine_id_to_name is None:

View file

@ -27,9 +27,11 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
from ..Messages.PrintJobUploadQueueFullMessage import PrintJobUploadQueueFullMessage from ..Messages.PrintJobUploadQueueFullMessage import PrintJobUploadQueueFullMessage
from ..Messages.PrintJobUploadPrinterInactiveMessage import PrintJobUploadPrinterInactiveMessage
from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Models.Http.CloudClusterStatus import CloudClusterStatus from ..Models.Http.CloudClusterStatus import CloudClusterStatus
from ..Models.Http.CloudError import CloudError
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
from ..Models.Http.CloudPrintResponse import CloudPrintResponse from ..Models.Http.CloudPrintResponse import CloudPrintResponse
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
@ -87,7 +89,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
address="", address="",
connection_type=ConnectionType.CloudConnection, connection_type=ConnectionType.CloudConnection,
properties=properties, properties=properties,
parent=parent parent=parent,
active=cluster.display_status != "inactive"
) )
self._api = api_client self._api = api_client
@ -190,6 +193,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._received_print_jobs = status.print_jobs self._received_print_jobs = status.print_jobs
self._updatePrintJobs(status.print_jobs) self._updatePrintJobs(status.print_jobs)
self._setActive(status.active)
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
@ -291,19 +296,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.writeFinished.emit() self.writeFinished.emit()
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): def _onPrintUploadSpecificError(self, error: CloudError, _: "QNetworkReply.NetworkError", http_error: int):
""" """
Displays a message when an error occurs specific to uploading print job (i.e. queue is full). Displays a message when an error occurs specific to uploading print job (i.e. queue is full).
""" """
error_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) if http_error == 409:
if error_code == 409: if error.code == "printerInactive":
PrintJobUploadQueueFullMessage().show() PrintJobUploadPrinterInactiveMessage().show()
else:
PrintJobUploadQueueFullMessage().show()
else: else:
PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send",
"Unknown error code when uploading print job: {0}", "Unknown error code when uploading print job: {0}",
error_code)).show() http_error)).show()
Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code)) Logger.log("w", "Upload of print job failed specifically with error code {}".format(http_error))
self._progress.hide() self._progress.hide()
self._pre_upload_print_job = None self._pre_upload_print_job = None

View file

@ -0,0 +1,20 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM import i18nCatalog
from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura")
class PrintJobUploadPrinterInactiveMessage(Message):
"""Message shown when uploading a print job to a cluster and the printer is inactive."""
def __init__(self) -> None:
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "The printer is inactive and cannot accept a new print job."),
title = I18N_CATALOG.i18nc("@info:title", "Printer inactive"),
lifetime = 10,
message_type=Message.MessageType.ERROR
)

View file

@ -10,7 +10,7 @@ class CloudClusterResponse(BaseModel):
"""Class representing a cloud connected cluster.""" """Class representing a cloud connected cluster."""
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, display_status: str, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None,
friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1,
capabilities: Optional[List[str]] = None, **kwargs) -> None: capabilities: Optional[List[str]] = None, **kwargs) -> None:
"""Creates a new cluster response object. """Creates a new cluster response object.
@ -20,6 +20,7 @@ class CloudClusterResponse(BaseModel):
:param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users.
:param is_online: Whether this cluster is currently connected to the cloud. :param is_online: Whether this cluster is currently connected to the cloud.
:param status: The status of the cluster authentication (active or inactive). :param status: The status of the cluster authentication (active or inactive).
:param display_status: The display status of the cluster.
:param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. :param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
:param host_internal_ip: The internal IP address of the host printer. :param host_internal_ip: The internal IP address of the host printer.
:param friendly_name: The human readable name of the host printer. :param friendly_name: The human readable name of the host printer.
@ -31,6 +32,7 @@ class CloudClusterResponse(BaseModel):
self.host_guid = host_guid self.host_guid = host_guid
self.host_name = host_name self.host_name = host_name
self.status = status self.status = status
self.display_status = display_status
self.is_online = is_online self.is_online = is_online
self.host_version = host_version self.host_version = host_version
self.host_internal_ip = host_internal_ip self.host_internal_ip = host_internal_ip
@ -51,5 +53,5 @@ class CloudClusterResponse(BaseModel):
Convenience function for printing when debugging. Convenience function for printing when debugging.
:return: A human-readable representation of the data in this object. :return: A human-readable representation of the data in this object.
""" """
return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "display_status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}})

View file

@ -14,6 +14,7 @@ class CloudClusterStatus(BaseModel):
def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]],
print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]],
generated_time: Union[str, datetime], generated_time: Union[str, datetime],
unavailable: bool = False,
**kwargs) -> None: **kwargs) -> None:
"""Creates a new cluster status model object. """Creates a new cluster status model object.
@ -23,6 +24,7 @@ class CloudClusterStatus(BaseModel):
""" """
self.generated_time = self.parseDate(generated_time) self.generated_time = self.parseDate(generated_time)
self.active = not unavailable
self.printers = self.parseModels(ClusterPrinterStatus, printers) self.printers = self.parseModels(ClusterPrinterStatus, printers)
self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs)
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -20,13 +20,23 @@ from ..BaseModel import BaseModel
class ClusterPrinterStatus(BaseModel): class ClusterPrinterStatus(BaseModel):
"""Class representing a cluster printer""" """Class representing a cluster printer"""
def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, def __init__(self,
status: str, unique_name: str, uuid: str, enabled: Optional[bool] = True,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], friendly_name: Optional[str] = "",
reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, machine_variant: Optional[str] = "",
firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, status: Optional[str] = "unknown",
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, unique_name: Optional[str] = "",
material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: uuid: Optional[str] = "",
configuration: Optional[List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]]] = None,
firmware_version: Optional[str] = None,
ip_address: Optional[str] = None,
reserved_by: Optional[str] = "",
maintenance_required: Optional[bool] = False,
firmware_update_status: Optional[str] = "",
latest_available_firmware: Optional[str] = "",
build_plate: Optional[Union[Dict[str, Any], ClusterBuildPlate]] = None,
material_station: Optional[Union[Dict[str, Any], ClusterPrinterMaterialStation]] = None,
**kwargs) -> None:
""" """
Creates a new cluster printer status Creates a new cluster printer status
:param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled. :param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled.
@ -47,7 +57,7 @@ class ClusterPrinterStatus(BaseModel):
:param material_station: The material station that is on the printer. :param material_station: The material station that is on the printer.
""" """
self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) if configuration else []
self.enabled = enabled self.enabled = enabled
self.firmware_version = firmware_version self.firmware_version = firmware_version
self.friendly_name = friendly_name self.friendly_name = friendly_name
@ -70,7 +80,7 @@ class ClusterPrinterStatus(BaseModel):
:param controller: - The controller of the model. :param controller: - The controller of the model.
""" """
model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version) model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version or "")
self.updateOutputModel(model) self.updateOutputModel(model)
return model return model
@ -86,7 +96,8 @@ class ClusterPrinterStatus(BaseModel):
model.updateType(self.machine_variant) model.updateType(self.machine_variant)
model.updateState(self.status if self.enabled else "disabled") model.updateState(self.status if self.enabled else "disabled")
model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass")
model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) if self.ip_address:
model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address)))
if not model.printerConfiguration: if not model.printerConfiguration:
# Prevent accessing printer configuration when not available. # Prevent accessing printer configuration when not available.

View file

@ -46,10 +46,10 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
QUEUED_PRINT_JOBS_STATES = {"queued", "error"} QUEUED_PRINT_JOBS_STATES = {"queued", "error"}
def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType,
parent=None) -> None: parent=None, active: bool = True) -> None:
super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type,
parent=parent) parent=parent, active=active)
# Trigger the printersChanged signal when the private signal is triggered. # Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged) self.printersChanged.connect(self._clusterPrintersChanged)

View file

@ -16,6 +16,7 @@ Cura.ExpandablePopup
property bool isConnectedCloudPrinter: machineManager.activeMachineHasCloudConnection property bool isConnectedCloudPrinter: machineManager.activeMachineHasCloudConnection
property bool isCloudRegistered: machineManager.activeMachineHasCloudRegistration property bool isCloudRegistered: machineManager.activeMachineHasCloudRegistration
property bool isGroup: machineManager.activeMachineIsGroup property bool isGroup: machineManager.activeMachineIsGroup
property bool isActive: machineManager.activeMachineIsActive
property string machineName: { property string machineName: {
if (isNetworkPrinter && machineManager.activeMachineNetworkGroupName != "") if (isNetworkPrinter && machineManager.activeMachineNetworkGroupName != "")
{ {
@ -40,7 +41,14 @@ Cura.ExpandablePopup
} }
else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable)
{ {
return "printer_cloud_connected" if (isActive)
{
return "printer_cloud_connected"
}
else
{
return "printer_cloud_inactive"
}
} }
else if (isCloudRegistered) else if (isCloudRegistered)
{ {
@ -53,7 +61,7 @@ Cura.ExpandablePopup
} }
function getConnectionStatusMessage() { function getConnectionStatusMessage() {
if (connectionStatus == "printer_cloud_not_available") if (connectionStatus === "printer_cloud_not_available")
{ {
if(Cura.API.connectionStatus.isInternetReachable) if(Cura.API.connectionStatus.isInternetReachable)
{ {
@ -78,6 +86,10 @@ Cura.ExpandablePopup
return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.") return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.")
} }
} }
else if(connectionStatus === "printer_cloud_inactive")
{
return catalog.i18nc("@status", "This printer is deactivated and can not accept commands or jobs.")
}
else else
{ {
return "" return ""
@ -130,14 +142,18 @@ Cura.ExpandablePopup
source: source:
{ {
if (connectionStatus == "printer_connected") if (connectionStatus === "printer_connected")
{ {
return UM.Theme.getIcon("CheckBlueBG", "low") return UM.Theme.getIcon("CheckBlueBG", "low")
} }
else if (connectionStatus == "printer_cloud_connected" || connectionStatus == "printer_cloud_not_available") else if (connectionStatus === "printer_cloud_connected" || connectionStatus === "printer_cloud_not_available")
{ {
return UM.Theme.getIcon("CloudBadge", "low") return UM.Theme.getIcon("CloudBadge", "low")
} }
else if (connectionStatus === "printer_cloud_inactive")
{
return UM.Theme.getIcon("WarningBadge", "low")
}
else else
{ {
return "" return ""
@ -147,7 +163,21 @@ Cura.ExpandablePopup
width: UM.Theme.getSize("printer_status_icon").width width: UM.Theme.getSize("printer_status_icon").width
height: UM.Theme.getSize("printer_status_icon").height height: UM.Theme.getSize("printer_status_icon").height
color: connectionStatus == "printer_cloud_not_available" ? UM.Theme.getColor("cloud_unavailable") : UM.Theme.getColor("primary") color:
{
if (connectionStatus === "printer_cloud_not_available")
{
return UM.Theme.getColor("cloud_unavailable")
}
else if(connectionStatus === "printer_cloud_inactive")
{
return UM.Theme.getColor("cloud_inactive")
}
else
{
return UM.Theme.getColor("primary")
}
}
visible: (isNetworkPrinter || isCloudRegistered) && source != "" visible: (isNetworkPrinter || isCloudRegistered) && source != ""

View file

@ -498,6 +498,7 @@
"monitor_carousel_dot_current": [119, 119, 119, 255], "monitor_carousel_dot_current": [119, 119, 119, 255],
"cloud_unavailable": [153, 153, 153, 255], "cloud_unavailable": [153, 153, 153, 255],
"cloud_inactive": [253, 209, 58, 255],
"connection_badge_background": [255, 255, 255, 255], "connection_badge_background": [255, 255, 255, 255],
"warning_badge_background": [0, 0, 0, 255], "warning_badge_background": [0, 0, 0, 255],
"error_badge_background": [255, 255, 255, 255], "error_badge_background": [255, 255, 255, 255],