From 6fed6b824c104deb722bc430a8073d69acd76e09 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 30 Aug 2022 15:48:10 +0200 Subject: [PATCH] Added stub for AbstractCloudOutputDevice It doesn't actually allow you to send a print, but it does ask some info from the API CURA-8463 --- .../src/Cloud/AbstractCloudOutputDevice.py | 87 +++++++++++++++++++ .../src/Cloud/CloudApiClient.py | 19 +++- .../src/Cloud/CloudOutputDeviceManager.py | 55 +++++++++--- .../src/Models/Http/CloudClusterResponse.py | 1 - 4 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py new file mode 100644 index 0000000000..566eae2ed9 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Cloud/AbstractCloudOutputDevice.py @@ -0,0 +1,87 @@ +from time import time +from typing import List + +from PyQt6.QtCore import QObject +from PyQt6.QtNetwork import QNetworkReply + +from UM import i18nCatalog +from UM.Logger import Logger +from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState +from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from .CloudApiClient import CloudApiClient +from ..Models.Http.CloudClusterResponse import CloudClusterResponse +from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice + +I18N_CATALOG = i18nCatalog("cura") + + +class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): + API_CHECK_INTERVAL = 10.0 # seconds + + def __init__(self, api_client: CloudApiClient, printer_type: str, parent: QObject = None) -> None: + + self._api = api_client + properties = { + #b"address": cluster.host_internal_ip.encode() if cluster.host_internal_ip else b"", + # b"name": cluster.friendly_name.encode() if cluster.friendly_name else b"", + ##b"firmware_version": cluster.host_version.encode() if cluster.host_version else b"", + b"printer_type": printer_type.encode() + #b"cluster_size": str(cluster.printer_count).encode() if cluster.printer_count else b"1" + } + super().__init__( + device_id=f"ABSTRACT_{printer_type}", + address="", + connection_type=ConnectionType.CloudConnection, + properties=properties, + parent=parent + ) + + print("CREATING ABSTRACT CLOUD OUTPUT DEVIIICEEEEEE") + self._setInterfaceElements() + + def connect(self) -> None: + """Connects this device.""" + + if self.isConnected(): + return + Logger.log("i", "Attempting to connect AbstractCloudOutputDevice %s", self.key) + super().connect() + + #CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) + self._update() + + def disconnect(self) -> None: + """Disconnects the device""" + + if not self.isConnected(): + return + super().disconnect() + + def _update(self) -> None: + """Called when the network data should be updated.""" + + super()._update() + if time() - self._time_of_last_request < self.API_CHECK_INTERVAL: + return # avoid calling the cloud too often + self._time_of_last_request = time() + if self._api.account.isLoggedIn: + self.setAuthenticationState(AuthState.Authenticated) + self._last_request_time = time() + self._api.getClustersByMachineType(self.printerType, self._onCompleted, self._onError) + else: + self.setAuthenticationState(AuthState.NotAuthenticated) + + def _setInterfaceElements(self) -> None: + """Set all the interface elements and texts for this output device.""" + + self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. + self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via cloud")) + self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via cloud")) + self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via cloud")) + + def _onCompleted(self, clusters: List[CloudClusterResponse]) -> None: + self._responseReceived() + # Todo: actually handle the data that we get back! + + def _onError(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: + pass diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 80029414d9..89f9e9d449 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -61,7 +61,6 @@ class CloudApiClient: @property def account(self) -> Account: """Gets the account used for the API.""" - return self._account def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: @@ -77,6 +76,24 @@ class CloudApiClient: error_callback = failed, timeout = self.DEFAULT_REQUEST_TIMEOUT) + def getClustersByMachineType(self, machine_type, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: + # HACK: There is something weird going on with the API, as it reports printer types in formats like + # "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some + # conversion! + + machine_type = machine_type.replace("_plus", "+") + machine_type = machine_type.replace("_", " ") + machine_type = machine_type.replace("ultimaker", "ultimaker ") + machine_type = machine_type.replace(" ", " ") + machine_type = machine_type.title() + + url = f"{self.CLUSTER_API_ROOT}/clusters?machine_variant={machine_type}" + self._http.get(url, + scope=self._scope, + callback=self._parseCallback(on_finished, CloudClusterResponse, failed), + error_callback=failed, + timeout=self.DEFAULT_REQUEST_TIMEOUT) + def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: """Retrieves the status of the given cluster. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index d7f67498f3..846ef0a8ec 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -19,6 +19,7 @@ from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To upda from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.GlobalStack import GlobalStack from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT +from .AbstractCloudOutputDevice import AbstractCloudOutputDevice from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage @@ -49,6 +50,8 @@ class CloudOutputDeviceManager: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters: Dict[str, CloudOutputDevice] = {} + self._abstract_clusters: Dict[str, AbstractCloudOutputDevice] = {} + # Dictionary containing all the cloud printers loaded in Cura self._um_cloud_printers: Dict[str, GlobalStack] = {} @@ -189,6 +192,10 @@ class CloudOutputDeviceManager: for cluster_data in discovered_clusters: output_device = CloudOutputDevice(self._api, cluster_data) + + if cluster_data.printer_type not in self._abstract_clusters: + self._abstract_clusters[cluster_data.printer_type] = AbstractCloudOutputDevice(self._api, cluster_data.printer_type) + # If the machine already existed before, it will be present in the host_guid_map if cluster_data.host_guid in host_guid_map: machine = machine_manager.getMachine(output_device.printerType, {self.META_HOST_GUID: cluster_data.host_guid}) @@ -373,22 +380,33 @@ class CloudOutputDeviceManager: if not active_machine: return + # Check if we should directly connect with a "normal" CloudOutputDevice or that we should connect to an + # 'abstract' one output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() - stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) - local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) + if active_machine.getMetaDataEntry("is_abstract_machine") != "True": + stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) + local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) - # Copy of the device list, to prevent modifying the list while iterating, if a device gets added asynchronously. - remote_cluster_copy: List[CloudOutputDevice] = list(self._remote_clusters.values()) - for device in remote_cluster_copy: - if device.key == stored_cluster_id: - # Connect to it if the stored ID matches. - self._connectToOutputDevice(device, active_machine) - elif local_network_key and device.matchesNetworkKey(local_network_key): - # Connect to it if we can match the local network key that was already present. - self._connectToOutputDevice(device, active_machine) - elif device.key in output_device_manager.getOutputDeviceIds(): - # Remove device if it is not meant for the active machine. - output_device_manager.removeOutputDevice(device.key) + # Copy of the device list, to prevent modifying the list while iterating, if a device gets added asynchronously. + remote_cluster_copy: List[CloudOutputDevice] = list(self._remote_clusters.values()) + for device in remote_cluster_copy: + if device.key == stored_cluster_id: + # Connect to it if the stored ID matches. + self._connectToOutputDevice(device, active_machine) + elif local_network_key and device.matchesNetworkKey(local_network_key): + # Connect to it if we can match the local network key that was already present. + self._connectToOutputDevice(device, active_machine) + elif device.key in output_device_manager.getOutputDeviceIds(): + # Remove device if it is not meant for the active machine. + output_device_manager.removeOutputDevice(device.key) + else: # Abstract it is! + remote_abstract_cluster_copy: List[CloudOutputDevice] = list(self._abstract_clusters.values()) + for device in remote_abstract_cluster_copy: + if device.printerType == active_machine.definition.getId(): + print("Found the device to activate", device) + self._connectToAbstractOutputDevice(device, active_machine) + else: + output_device_manager.removeOutputDevice(device.key) def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) @@ -405,6 +423,15 @@ class CloudOutputDeviceManager: machine.setMetaDataEntry("removal_warning", removal_warning_string) machine.addConfiguredConnectionType(device.connectionType.value) + def _connectToAbstractOutputDevice(self, device: AbstractCloudOutputDevice, machine: GlobalStack) -> None: + if not device.isConnected(): + device.connect() + machine.addConfiguredConnectionType(device.connectionType.value) + + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + if device.key not in output_device_manager.getOutputDeviceIds(): + output_device_manager.addOutputDevice(device) + def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: """Connects to an output device and makes sure it is registered in the output device manager.""" diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index ce6dd1de4d..c8f3be282e 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -8,7 +8,6 @@ from ..BaseModel import BaseModel class CloudClusterResponse(BaseModel): """Class representing a cloud connected cluster.""" - 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, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1,