Linux PyQt tray application to control OctoPrint instances
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # depends on:
  5. # - python-pyqt5
  6. # - curl
  7. # - xdg-open
  8. import json
  9. import subprocess
  10. import sys
  11. import os
  12. import threading
  13. import time
  14. from PyQt5 import QtWidgets, QtGui, QtCore
  15. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox
  16. from PyQt5.QtGui import QIcon, QPixmap
  17. from PyQt5.QtCore import QCoreApplication, QSettings
  18. class OctoTray():
  19. name = "OctoTray"
  20. vendor = "xythobuz"
  21. version = "0.1"
  22. iconPath = "/usr/share/pixmaps/"
  23. iconName = "octotray_icon.png"
  24. # 0=host, 1=key
  25. # (2=method, 3=menu, 4...=actions)
  26. printers = [
  27. [ "PRINTER_HOST_HERE", "PRINTER_API_KEY_HERE" ]
  28. ]
  29. statesWithWarning = [
  30. "Printing", "Pausing", "Paused"
  31. ]
  32. def __init__(self):
  33. app = QtWidgets.QApplication(sys.argv)
  34. QCoreApplication.setApplicationName(self.name)
  35. if not QSystemTrayIcon.isSystemTrayAvailable():
  36. print("System Tray is not available on this platform!")
  37. sys.exit(0)
  38. self.menu = QMenu()
  39. for p in self.printers:
  40. method = self.getMethod(p[0], p[1])
  41. print("Printer " + p[0] + " has method " + method)
  42. p.append(method)
  43. if method == "unknown":
  44. continue
  45. menu = QMenu(self.getName(p[0], p[1]))
  46. p.append(menu)
  47. self.menu.addMenu(menu)
  48. action = QAction("Turn on")
  49. action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
  50. p.append(action)
  51. menu.addAction(action)
  52. action = QAction("Turn off")
  53. action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
  54. p.append(action)
  55. menu.addAction(action)
  56. action = QAction("Get Status")
  57. action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
  58. p.append(action)
  59. menu.addAction(action)
  60. action = QAction("Open Web UI")
  61. action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
  62. p.append(action)
  63. menu.addAction(action)
  64. self.quitAction = QAction("&Quit")
  65. self.quitAction.triggered.connect(self.exit)
  66. self.menu.addAction(self.quitAction)
  67. iconPathName = ""
  68. if os.path.isfile(self.iconName):
  69. iconPathName = self.iconName
  70. elif os.path.isfile(self.iconPath + self.iconName):
  71. iconPathName = self.iconPath + self.iconName
  72. else:
  73. print("no icon found")
  74. icon = QIcon()
  75. if iconPathName != "":
  76. pic = QPixmap(32, 32)
  77. pic.load(iconPathName)
  78. icon = QIcon(pic)
  79. trayIcon = QSystemTrayIcon(icon)
  80. trayIcon.setToolTip(self.name + " " + self.version)
  81. trayIcon.setContextMenu(self.menu)
  82. trayIcon.setVisible(True)
  83. sys.exit(app.exec_())
  84. def openBrowser(self, url):
  85. os.system("xdg-open http://" + url)
  86. def showDialog(self, title, text1, text2, question, warning):
  87. msg = QMessageBox()
  88. if warning:
  89. msg.setIcon(QMessageBox.Warning)
  90. else:
  91. msg.setIcon(QMessageBox.Information)
  92. msg.setWindowTitle(title)
  93. msg.setText(text1)
  94. if text2 is not None:
  95. msg.setInformativeText(text2)
  96. if question:
  97. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  98. else:
  99. msg.setStandardButtons(QMessageBox.Ok)
  100. retval = msg.exec_()
  101. if retval == QMessageBox.Yes:
  102. return True
  103. return False
  104. def sendRequest(self, host, headers, path, content = None):
  105. cmdline = 'curl -s'
  106. for h in headers:
  107. cmdline += " -H \"" + h + "\""
  108. if content == None:
  109. cmdline += " -X GET"
  110. else:
  111. cmdline += " -X POST"
  112. cmdline += " -d '" + content + "'"
  113. cmdline += " http://" + host + "/api/" + path
  114. r = subprocess.run(cmdline, shell=True, capture_output=True, timeout=10, text=True)
  115. return r.stdout
  116. def sendPostRequest(self, host, key, path, content):
  117. headers = [ "Content-Type: application/json",
  118. "X-Api-Key: " + key ]
  119. return self.sendRequest(host, headers, path, content)
  120. def sendGetRequest(self, host, key, path):
  121. headers = [ "X-Api-Key: " + key ]
  122. return self.sendRequest(host, headers, path)
  123. def getState(self, host, key):
  124. r = self.sendGetRequest(host, key, "job")
  125. try:
  126. rd = json.loads(r)
  127. if "state" in rd:
  128. return rd["state"]
  129. except json.JSONDecodeError:
  130. pass
  131. return "Unknown"
  132. def getProgress(self, host, key):
  133. r = self.sendGetRequest(host, key, "job")
  134. try:
  135. rd = json.loads(r)
  136. if "progress" in rd:
  137. return rd["progress"]
  138. except json.JSONDecodeError:
  139. pass
  140. return "Unknown"
  141. def getName(self, host, key):
  142. r = self.sendGetRequest(host, key, "printerprofiles")
  143. try:
  144. rd = json.loads(r)
  145. if "profiles" in rd:
  146. p = next(iter(rd["profiles"]))
  147. if "name" in rd["profiles"][p]:
  148. return rd["profiles"][p]["name"]
  149. except json.JSONDecodeError:
  150. pass
  151. return host
  152. def getMethod(self, host, key):
  153. r = self.sendGetRequest(host, key, "plugin/psucontrol")
  154. try:
  155. rd = json.loads(r)
  156. if "isPSUOn" in rd:
  157. return "psucontrol"
  158. except json.JSONDecodeError:
  159. pass
  160. r = self.sendGetRequest(host, key, "system/commands/custom")
  161. try:
  162. rd = json.loads(r)
  163. for c in rd:
  164. if "action" in c:
  165. if (c["action"] == "all off") or (c["action"] == "all on"):
  166. return "system"
  167. except json.JSONDecodeError:
  168. pass
  169. return "unknown"
  170. def setPSUControl(self, host, key, state):
  171. cmd = "turnPSUOff"
  172. if state:
  173. cmd = "turnPSUOn"
  174. return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
  175. def setSystemCommand(self, host, key, state):
  176. cmd = "all%20off"
  177. if state:
  178. cmd = "all%20on"
  179. return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
  180. def setPrinter(self, host, key, method, state):
  181. if method == "psucontrol":
  182. return self.setPSUControl(host, key, state)
  183. elif method == "system":
  184. return self.setSystemCommand(host, key, state)
  185. else:
  186. return "error"
  187. def exit(self):
  188. QCoreApplication.quit()
  189. def printerOnAction(self, item):
  190. self.setPrinter(item[0], item[1], item[2], True)
  191. def printerOffAction(self, item):
  192. state = self.getState(item[0], item[1])
  193. if state in self.statesWithWarning:
  194. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == True:
  195. self.setPrinter(item[0], item[1], item[2], False)
  196. else:
  197. self.setPrinter(item[0], item[1], item[2], False)
  198. def printerWebAction(self, item):
  199. self.openBrowser(item[0])
  200. def printerStatusAction(self, item):
  201. progress = self.getProgress(item[0], item[1])
  202. s = ""
  203. warning = False
  204. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  205. s = "%.1f%% Completion\n" % progress["completion"]
  206. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  207. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left\n"
  208. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  209. s = "No job is currently running"
  210. else:
  211. s = "Could not read printer status!"
  212. warning = True
  213. self.showDialog("OctoTray Status", s, None, False, warning)
  214. if __name__ == "__main__":
  215. tray = OctoTray()