#!/usr/bin/env python3 # OctoTray Linux Qt System Tray OctoPrint client # # depends on: # - python-pyqt5 # - curl # - xdg-open import json import subprocess import sys import os import threading import time from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtCore import QCoreApplication, QSettings class OctoTray(): name = "OctoTray" vendor = "xythobuz" version = "0.1" iconPath = "/usr/share/pixmaps/" iconName = "octotray_icon.png" # 0=host, 1=key # (2=method, 3=menu, 4...=actions) printers = [ [ "PRINTER_HOST_HERE", "PRINTER_API_KEY_HERE" ] ] statesWithWarning = [ "Printing", "Pausing", "Paused" ] def __init__(self): app = QtWidgets.QApplication(sys.argv) QCoreApplication.setApplicationName(self.name) if not QSystemTrayIcon.isSystemTrayAvailable(): print("System Tray is not available on this platform!") sys.exit(0) self.menu = QMenu() for p in self.printers: method = self.getMethod(p[0], p[1]) print("Printer " + p[0] + " has method " + method) p.append(method) if method == "unknown": continue menu = QMenu(self.getName(p[0], p[1])) p.append(menu) self.menu.addMenu(menu) action = QAction("Turn on") action.triggered.connect(lambda chk, x=p: self.printerOnAction(x)) p.append(action) menu.addAction(action) action = QAction("Turn off") action.triggered.connect(lambda chk, x=p: self.printerOffAction(x)) p.append(action) menu.addAction(action) action = QAction("Get Status") action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x)) p.append(action) menu.addAction(action) action = QAction("Open Web UI") action.triggered.connect(lambda chk, x=p: self.printerWebAction(x)) p.append(action) menu.addAction(action) self.quitAction = QAction("&Quit") self.quitAction.triggered.connect(self.exit) self.menu.addAction(self.quitAction) iconPathName = "" if os.path.isfile(self.iconName): iconPathName = self.iconName elif os.path.isfile(self.iconPath + self.iconName): iconPathName = self.iconPath + self.iconName else: print("no icon found") icon = QIcon() if iconPathName != "": pic = QPixmap(32, 32) pic.load(iconPathName) icon = QIcon(pic) trayIcon = QSystemTrayIcon(icon) trayIcon.setToolTip(self.name + " " + self.version) trayIcon.setContextMenu(self.menu) trayIcon.setVisible(True) sys.exit(app.exec_()) def openBrowser(self, url): os.system("xdg-open http://" + url) def showDialog(self, title, text1, text2, question, warning): msg = QMessageBox() if warning: msg.setIcon(QMessageBox.Warning) else: msg.setIcon(QMessageBox.Information) msg.setWindowTitle(title) msg.setText(text1) if text2 is not None: msg.setInformativeText(text2) if question: msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) else: msg.setStandardButtons(QMessageBox.Ok) retval = msg.exec_() if retval == QMessageBox.Yes: return True return False def sendRequest(self, host, headers, path, content = None): cmdline = 'curl -s' for h in headers: cmdline += " -H \"" + h + "\"" if content == None: cmdline += " -X GET" else: cmdline += " -X POST" cmdline += " -d '" + content + "'" cmdline += " http://" + host + "/api/" + path r = subprocess.run(cmdline, shell=True, capture_output=True, timeout=10, text=True) return r.stdout def sendPostRequest(self, host, key, path, content): headers = [ "Content-Type: application/json", "X-Api-Key: " + key ] return self.sendRequest(host, headers, path, content) def sendGetRequest(self, host, key, path): headers = [ "X-Api-Key: " + key ] return self.sendRequest(host, headers, path) def getState(self, host, key): r = self.sendGetRequest(host, key, "job") try: rd = json.loads(r) if "state" in rd: return rd["state"] except json.JSONDecodeError: pass return "Unknown" def getProgress(self, host, key): r = self.sendGetRequest(host, key, "job") try: rd = json.loads(r) if "progress" in rd: return rd["progress"] except json.JSONDecodeError: pass return "Unknown" def getName(self, host, key): r = self.sendGetRequest(host, key, "printerprofiles") try: rd = json.loads(r) if "profiles" in rd: p = next(iter(rd["profiles"])) if "name" in rd["profiles"][p]: return rd["profiles"][p]["name"] except json.JSONDecodeError: pass return host def getMethod(self, host, key): r = self.sendGetRequest(host, key, "plugin/psucontrol") try: rd = json.loads(r) if "isPSUOn" in rd: return "psucontrol" except json.JSONDecodeError: pass r = self.sendGetRequest(host, key, "system/commands/custom") try: rd = json.loads(r) for c in rd: if "action" in c: if (c["action"] == "all off") or (c["action"] == "all on"): return "system" except json.JSONDecodeError: pass return "unknown" def setPSUControl(self, host, key, state): cmd = "turnPSUOff" if state: cmd = "turnPSUOn" return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }') def setSystemCommand(self, host, key, state): cmd = "all%20off" if state: cmd = "all%20on" return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '') def setPrinter(self, host, key, method, state): if method == "psucontrol": return self.setPSUControl(host, key, state) elif method == "system": return self.setSystemCommand(host, key, state) else: return "error" def exit(self): QCoreApplication.quit() def printerOnAction(self, item): self.setPrinter(item[0], item[1], item[2], True) def printerOffAction(self, item): state = self.getState(item[0], item[1]) if state in self.statesWithWarning: if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == True: self.setPrinter(item[0], item[1], item[2], False) else: self.setPrinter(item[0], item[1], item[2], False) def printerWebAction(self, item): self.openBrowser(item[0]) def printerStatusAction(self, item): progress = self.getProgress(item[0], item[1]) s = "" warning = False if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None): s = "%.1f%% Completion\n" % progress["completion"] s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n" s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left\n" elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress): s = "No job is currently running" else: s = "Could not read printer status!" warning = True self.showDialog("OctoTray Status", s, None, False, warning) if __name__ == "__main__": tray = OctoTray()