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.

octotray.py 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # depends on:
  5. # - python-pyqt5
  6. #
  7. # see also:
  8. # https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
  9. # https://stackoverflow.com/a/22618496
  10. import json
  11. import sys
  12. import os
  13. import time
  14. import string
  15. import urllib.parse
  16. import urllib.request
  17. from os import path
  18. from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
  19. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout, QTableWidget, QTableWidgetItem, QPushButton
  20. from PyQt5.QtGui import QIcon, QPixmap, QImageReader, QDesktopServices, QFontDatabase
  21. from PyQt5.QtCore import QCoreApplication, QSettings, QUrl, QTimer, QSize, Qt, QSettings
  22. class SettingsWindow(QWidget):
  23. columns = [ "Hostname", "API Key", "Tool Preheat", "Bed Preheat" ]
  24. presets = [ "octopi.local", "000000000_API_KEY_HERE_000000000", "0", "0" ]
  25. def __init__(self, parent, *args, **kwargs):
  26. super(SettingsWindow, self).__init__(*args, **kwargs)
  27. self.parent = parent
  28. self.setWindowTitle(parent.name + " Settings")
  29. self.setWindowIcon(parent.icon)
  30. box = QVBoxLayout()
  31. self.setLayout(box)
  32. helpText = "Usage:\n"
  33. helpText += "1st Column: Printer Hostname or IP address\n"
  34. helpText += "2nd Column: OctoPrint API Key (32 char hexadecimal)\n"
  35. helpText += "3rd Column: Tool Preheat Temperature (0 to disable)\n"
  36. helpText += "4th Column: Bed Preheat Temperature (0 to disable)"
  37. self.helpText = QLabel(helpText)
  38. box.addWidget(self.helpText, 0)
  39. box.setAlignment(self.helpText, Qt.AlignHCenter)
  40. buttons = QHBoxLayout()
  41. box.addLayout(buttons, 0)
  42. self.add = QPushButton("&Add Printer")
  43. self.add.clicked.connect(self.addPrinter)
  44. buttons.addWidget(self.add)
  45. self.remove = QPushButton("&Remove Printer")
  46. self.remove.clicked.connect(self.removePrinter)
  47. buttons.addWidget(self.remove)
  48. printers = self.parent.readSettings()
  49. self.rows = len(printers)
  50. self.table = QTableWidget(self.rows, len(self.columns))
  51. box.addWidget(self.table, 1)
  52. for i in range(0, self.rows):
  53. p = printers[i]
  54. for j in range(0, len(self.columns)):
  55. text = p[j]
  56. if (j >= 2) and (j <= 3) and (text == None):
  57. text = "0"
  58. item = QTableWidgetItem(text)
  59. self.table.setItem(i, j, item)
  60. if j == 1:
  61. font = item.font()
  62. font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
  63. item.setFont(font)
  64. buttons2 = QHBoxLayout()
  65. box.addLayout(buttons2, 0)
  66. self.up = QPushButton("Move &Up")
  67. self.up.clicked.connect(self.moveUp)
  68. buttons2.addWidget(self.up)
  69. self.down = QPushButton("Move &Down")
  70. self.down.clicked.connect(self.moveDown)
  71. buttons2.addWidget(self.down)
  72. self.openWeb = QPushButton("&Open Web UI of selected")
  73. self.openWeb.clicked.connect(self.openWebUI)
  74. box.addWidget(self.openWeb, 0)
  75. self.table.setHorizontalHeaderLabels(self.columns)
  76. self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
  77. self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
  78. self.table.resizeColumnsToContents()
  79. if self.rows <= 0:
  80. self.addPrinter()
  81. def tableToList(self):
  82. printers = []
  83. for i in range(0, self.rows):
  84. p = []
  85. for j in range(0, len(self.columns)):
  86. text = self.table.item(i, j).text()
  87. if (j >= 2) and (j <= 3) and (text == "0"):
  88. text = None
  89. p.append(text)
  90. printers.append(p)
  91. return printers
  92. def settingsValid(self, printers):
  93. for p in printers:
  94. # p[0] needs to be valid hostname or IP
  95. # TODO
  96. # p[1] needs to be valid API key (hexadecimal, 32 chars)
  97. if (len(p[1]) != 32) or not all(c in string.hexdigits for c in p[1]):
  98. return (False, "API Key not 32-digit hexadecimal")
  99. # p[2] and p[3] need to be integer temperatures (0...999)
  100. for s in [ p[2], p[3] ]:
  101. if s == None:
  102. s = "0"
  103. if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
  104. return (False, "Temperature not a number from 0...999")
  105. return (True, "")
  106. def closeEvent(self, event):
  107. oldPrinters = [item[0:len(self.columns)] for item in self.parent.printers]
  108. newPrinters = self.tableToList()
  109. valid, errorText = self.settingsValid(newPrinters)
  110. if valid == False:
  111. r = self.parent.showDialog(self.parent.name + " Settings Invalid", errorText + "!", "Do you want to edit it again?", True, True, False)
  112. if r == True:
  113. event.ignore()
  114. return
  115. else:
  116. self.parent.removeSettingsWindow()
  117. return
  118. if oldPrinters != newPrinters:
  119. r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new list of printers?", "This will restart the application!", True, False, False)
  120. if r == True:
  121. self.parent.writeSettings(newPrinters)
  122. self.parent.restartApp()
  123. self.parent.removeSettingsWindow()
  124. def addPrinter(self):
  125. self.rows += 1
  126. self.table.setRowCount(self.rows)
  127. for i in range(0, len(self.columns)):
  128. item = QTableWidgetItem(self.presets[i])
  129. self.table.setItem(self.rows - 1, i, item)
  130. if i == 1:
  131. font = item.font()
  132. font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
  133. item.setFont(font)
  134. self.table.resizeColumnsToContents()
  135. self.table.setCurrentItem(self.table.item(self.rows - 1, 0))
  136. def removePrinter(self):
  137. r = self.table.currentRow()
  138. if (r >= 0) and (r < self.rows):
  139. self.rows -= 1
  140. self.table.removeRow(r)
  141. self.table.setCurrentItem(self.table.item(min(r, self.rows - 1), 0))
  142. def moveUp(self):
  143. i = self.table.currentRow()
  144. if i <= 0:
  145. return
  146. host = self.table.item(i, 0).text()
  147. key = self.table.item(i, 1).text()
  148. self.table.item(i, 0).setText(self.table.item(i - 1, 0).text())
  149. self.table.item(i, 1).setText(self.table.item(i - 1, 1).text())
  150. self.table.item(i - 1, 0).setText(host)
  151. self.table.item(i - 1, 1).setText(key)
  152. self.table.setCurrentItem(self.table.item(i - 1, 0))
  153. def moveDown(self):
  154. i = self.table.currentRow()
  155. if i >= (self.rows - 1):
  156. return
  157. host = self.table.item(i, 0).text()
  158. key = self.table.item(i, 1).text()
  159. self.table.item(i, 0).setText(self.table.item(i + 1, 0).text())
  160. self.table.item(i, 1).setText(self.table.item(i + 1, 1).text())
  161. self.table.item(i + 1, 0).setText(host)
  162. self.table.item(i + 1, 1).setText(key)
  163. self.table.setCurrentItem(self.table.item(i + 1, 0))
  164. def openWebUI(self):
  165. host = self.table.item(self.table.currentRow(), 0).text()
  166. self.parent.openBrowser(host)
  167. class AspectRatioPixmapLabel(QLabel):
  168. def __init__(self, *args, **kwargs):
  169. super(AspectRatioPixmapLabel, self).__init__(*args, **kwargs)
  170. self.setMinimumSize(1, 1)
  171. self.setScaledContents(False)
  172. self.pix = QPixmap(0, 0)
  173. def setPixmap(self, p):
  174. self.pix = p
  175. super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
  176. def heightForWidth(self, width):
  177. if self.pix.isNull():
  178. return self.height()
  179. else:
  180. return (self.pix.height() * width) / self.pix.width()
  181. def sizeHint(self):
  182. w = self.width()
  183. return QSize(int(w), int(self.heightForWidth(w)))
  184. def scaledPixmap(self):
  185. return self.pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
  186. def resizeEvent(self, e):
  187. if not self.pix.isNull():
  188. super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
  189. class CamWindow(QWidget):
  190. reloadDelayDefault = 1000 # in ms
  191. statusDelay = 5 * 1000 # in ms
  192. reloadOn = True
  193. sliderFactor = 100
  194. def __init__(self, parent, printer, *args, **kwargs):
  195. super(CamWindow, self).__init__(*args, **kwargs)
  196. self.app = parent.app
  197. self.manager = parent.manager
  198. self.manager.finished.connect(self.handleResponse)
  199. self.parent = parent
  200. self.printer = printer
  201. self.host = self.printer[0]
  202. self.url = "http://" + self.host + ":8080/?action=snapshot"
  203. self.setWindowTitle(parent.name + " Webcam Stream")
  204. self.setWindowIcon(parent.icon)
  205. box = QVBoxLayout()
  206. self.setLayout(box)
  207. label = QLabel(self.url)
  208. box.addWidget(label, 0)
  209. box.setAlignment(label, Qt.AlignHCenter)
  210. self.img = AspectRatioPixmapLabel()
  211. self.img.setPixmap(QPixmap(640, 480))
  212. box.addWidget(self.img, 1)
  213. slide = QHBoxLayout()
  214. box.addLayout(slide, 0)
  215. self.slider = QSlider(Qt.Horizontal)
  216. self.slider.setMinimum(int(0 / self.sliderFactor))
  217. self.slider.setMaximum(int(2000 / self.sliderFactor))
  218. self.slider.setTickInterval(int(100 / self.sliderFactor))
  219. self.slider.setPageStep(int(100 / self.sliderFactor))
  220. self.slider.setSingleStep(int(100 / self.sliderFactor))
  221. self.slider.setTickPosition(QSlider.TicksBelow)
  222. self.slider.setValue(int(self.reloadDelayDefault / self.sliderFactor))
  223. self.slider.valueChanged.connect(self.sliderChanged)
  224. slide.addWidget(self.slider, 1)
  225. self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
  226. slide.addWidget(self.slideLabel, 0)
  227. self.statusLabel = QLabel("Status: unavailable")
  228. box.addWidget(self.statusLabel, 0)
  229. box.setAlignment(label, Qt.AlignHCenter)
  230. self.loadImage()
  231. self.loadStatus()
  232. def getHost(self):
  233. return self.host
  234. def sliderChanged(self):
  235. self.slideLabel.setText(str(self.slider.value() * self.sliderFactor) + "ms")
  236. def closeEvent(self, event):
  237. self.reloadOn = False
  238. self.url = ""
  239. self.parent.removeWebcamWindow(self)
  240. def scheduleLoadImage(self):
  241. if self.reloadOn:
  242. QTimer.singleShot(self.slider.value() * self.sliderFactor, self.loadImage)
  243. def scheduleLoadStatus(self):
  244. if self.reloadOn:
  245. QTimer.singleShot(self.statusDelay, self.loadStatus)
  246. def loadImage(self):
  247. url = QUrl(self.url)
  248. request = QtNetwork.QNetworkRequest(url)
  249. self.manager.get(request)
  250. def loadStatus(self):
  251. s = "Status: "
  252. t = self.parent.getTemperatureString(self.host, self.printer[1])
  253. if len(t) > 0:
  254. s += t
  255. else:
  256. s += "Unknown"
  257. progress = self.parent.getProgress(self.host, self.printer[1])
  258. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  259. s += " - %.1f%%" % progress["completion"]
  260. s += " - runtime "
  261. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
  262. s += " - "
  263. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  264. self.statusLabel.setText(s)
  265. self.scheduleLoadStatus()
  266. def handleResponse(self, reply):
  267. if reply.url().url() == self.url:
  268. if reply.error() == QtNetwork.QNetworkReply.NoError:
  269. reader = QImageReader(reply)
  270. reader.setAutoTransform(True)
  271. image = reader.read()
  272. if image != None:
  273. if image.colorSpace().isValid():
  274. image.convertToColorSpace(QColorSpace.SRgb)
  275. self.img.setPixmap(QPixmap.fromImage(image))
  276. self.scheduleLoadImage()
  277. else:
  278. print("Error decoding image: " + reader.errorString())
  279. else:
  280. print("Error loading image: " + reply.errorString())
  281. class OctoTray():
  282. name = "OctoTray"
  283. vendor = "xythobuz"
  284. version = "0.4"
  285. iconName = "octotray_icon.png"
  286. iconPaths = [
  287. path.abspath(path.dirname(__file__)),
  288. "data",
  289. "/usr/share/pixmaps",
  290. ".",
  291. "..",
  292. "../data"
  293. ]
  294. networkTimeout = 2.0 # in s
  295. # list of lists, inner lists contain printer data:
  296. # first elements as in SettingsWindow.columns
  297. # 0=host 1=key 2=tool-preheat 3=bed-preheat
  298. # rest used for system-commands, menu, actions
  299. printers = []
  300. statesWithWarning = [
  301. "Printing", "Pausing", "Paused"
  302. ]
  303. camWindows = []
  304. settingsWindow = None
  305. def __init__(self, app):
  306. QCoreApplication.setApplicationName(self.name)
  307. self.app = app
  308. if not QSystemTrayIcon.isSystemTrayAvailable():
  309. self.showDialog("OctoTray Error", "System Tray is not available on this platform!", "", False, False, True)
  310. sys.exit(0)
  311. self.manager = QtNetwork.QNetworkAccessManager()
  312. self.menu = QMenu()
  313. self.printers = self.readSettings()
  314. unknownCount = 0
  315. for p in self.printers:
  316. method = self.getMethod(p[0], p[1])
  317. print("Printer " + p[0] + " has method " + method)
  318. if method == "unknown":
  319. unknownCount += 1
  320. action = QAction(p[0])
  321. action.setEnabled(False)
  322. p.append(action)
  323. self.menu.addAction(action)
  324. continue
  325. commands = self.getSystemCommands(p[0], p[1])
  326. p.append(commands)
  327. menu = QMenu(self.getName(p[0], p[1]))
  328. p.append(menu)
  329. self.menu.addMenu(menu)
  330. if method == "psucontrol":
  331. action = QAction("Turn On PSU")
  332. action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
  333. p.append(action)
  334. menu.addAction(action)
  335. action = QAction("Turn Off PSU")
  336. action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
  337. p.append(action)
  338. menu.addAction(action)
  339. for i in range(0, len(commands)):
  340. action = QAction(commands[i].title())
  341. action.triggered.connect(lambda chk, x=p, y=i: self.printerSystemCommandAction(x, y))
  342. p.append(action)
  343. menu.addAction(action)
  344. if (p[2] != None) or (p[3] != None):
  345. menu.addSeparator()
  346. if p[2] != None:
  347. action = QAction("Preheat Tool")
  348. action.triggered.connect(lambda chk, x=p: self.printerHeatTool(x))
  349. p.append(action)
  350. menu.addAction(action)
  351. if p[3] != None:
  352. action = QAction("Preheat Bed")
  353. action.triggered.connect(lambda chk, x=p: self.printerHeatBed(x))
  354. p.append(action)
  355. menu.addAction(action)
  356. if (p[2] != None) or (p[3] != None):
  357. action = QAction("Cooldown")
  358. action.triggered.connect(lambda chk, x=p: self.printerCooldown(x))
  359. p.append(action)
  360. menu.addAction(action)
  361. menu.addSeparator()
  362. action = QAction("Get Status")
  363. action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
  364. p.append(action)
  365. menu.addAction(action)
  366. action = QAction("Show Webcam")
  367. action.triggered.connect(lambda chk, x=p: self.printerWebcamAction(x))
  368. p.append(action)
  369. menu.addAction(action)
  370. action = QAction("Open Web UI")
  371. action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
  372. p.append(action)
  373. menu.addAction(action)
  374. self.menu.addSeparator()
  375. self.settingsAction = QAction("&Settings")
  376. self.settingsAction.triggered.connect(self.showSettingsAction)
  377. self.menu.addAction(self.settingsAction)
  378. self.refreshAction = QAction("&Refresh")
  379. self.refreshAction.triggered.connect(self.restartApp)
  380. self.menu.addAction(self.refreshAction)
  381. self.quitAction = QAction("&Quit")
  382. self.quitAction.triggered.connect(self.exit)
  383. self.menu.addAction(self.quitAction)
  384. self.iconPathName = None
  385. for p in self.iconPaths:
  386. if os.path.isfile(path.join(p, self.iconName)):
  387. self.iconPathName = path.join(p, self.iconName)
  388. break
  389. if self.iconPathName == None:
  390. self.showDialog("OctoTray Error", "Icon file has not been found!", "", False, False, True)
  391. sys.exit(0)
  392. self.icon = QIcon()
  393. self.pic = QPixmap(32, 32)
  394. self.pic.load(self.iconPathName)
  395. self.icon = QIcon(self.pic)
  396. self.trayIcon = QSystemTrayIcon(self.icon)
  397. self.trayIcon.setToolTip(self.name + " " + self.version)
  398. self.trayIcon.setContextMenu(self.menu)
  399. self.trayIcon.setVisible(True)
  400. def readSettings(self):
  401. settings = QSettings(self.vendor, self.name)
  402. printers = []
  403. l = settings.beginReadArray("printers")
  404. for i in range(0, l):
  405. settings.setArrayIndex(i)
  406. p = []
  407. p.append(settings.value("host"))
  408. p.append(settings.value("key"))
  409. p.append(settings.value("tool_preheat"))
  410. p.append(settings.value("bed_preheat"))
  411. printers.append(p)
  412. settings.endArray()
  413. return printers
  414. def writeSettings(self, printers):
  415. settings = QSettings(self.vendor, self.name)
  416. settings.remove("printers")
  417. settings.beginWriteArray("printers")
  418. for i in range(0, len(printers)):
  419. p = printers[i]
  420. settings.setArrayIndex(i)
  421. settings.setValue("host", p[0])
  422. settings.setValue("key", p[1])
  423. settings.setValue("tool_preheat", p[2])
  424. settings.setValue("bed_preheat", p[3])
  425. settings.endArray()
  426. del settings
  427. def openBrowser(self, url):
  428. QDesktopServices.openUrl(QUrl("http://" + url))
  429. def showDialog(self, title, text1, text2 = "", question = False, warning = False, error = False):
  430. msg = QMessageBox()
  431. if error:
  432. msg.setIcon(QMessageBox.Critical)
  433. elif warning:
  434. msg.setIcon(QMessageBox.Warning)
  435. elif question:
  436. msg.setIcon(QMessageBox.Question)
  437. else:
  438. msg.setIcon(QMessageBox.Information)
  439. msg.setWindowTitle(title)
  440. msg.setText(text1)
  441. if text2 is not None:
  442. msg.setInformativeText(text2)
  443. if question:
  444. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  445. else:
  446. msg.setStandardButtons(QMessageBox.Ok)
  447. retval = msg.exec_()
  448. if retval == QMessageBox.Yes:
  449. return True
  450. else:
  451. return False
  452. def sendRequest(self, host, headers, path, content = None):
  453. url = "http://" + host + "/api/" + path
  454. if content == None:
  455. request = urllib.request.Request(url, None, headers)
  456. else:
  457. data = content.encode('ascii')
  458. request = urllib.request.Request(url, data, headers)
  459. try:
  460. with urllib.request.urlopen(request, None, self.networkTimeout) as response:
  461. text = response.read()
  462. return text
  463. except (urllib.error.URLError, urllib.error.HTTPError) as error:
  464. print("Error requesting URL \"" + url + "\": \"" + str(error) + "\"")
  465. return "error"
  466. except socket.timeout:
  467. print("Timeout waiting for response to \"" + url + "\"")
  468. return "timeout"
  469. def sendPostRequest(self, host, key, path, content):
  470. headers = {
  471. "Content-Type": "application/json",
  472. "X-Api-Key": key
  473. }
  474. return self.sendRequest(host, headers, path, content)
  475. def sendGetRequest(self, host, key, path):
  476. headers = {
  477. "X-Api-Key": key
  478. }
  479. return self.sendRequest(host, headers, path)
  480. def getTemperatureIsSafe(self, host, key):
  481. r = self.sendGetRequest(host, key, "printer")
  482. try:
  483. rd = json.loads(r)
  484. if "temperature" in rd:
  485. if ("tool0" in rd["temperature"]) and ("actual" in rd["temperature"]["tool0"]):
  486. if rd["temperature"]["tool0"]["actual"] > 50.0:
  487. return False
  488. if ("tool1" in rd["temperature"]) and ("actual" in rd["temperature"]["tool1"]):
  489. if rd["temperature"]["tool1"]["actual"] > 50.0:
  490. return False
  491. except json.JSONDecodeError:
  492. pass
  493. return True
  494. def getTemperatureString(self, host, key):
  495. r = self.sendGetRequest(host, key, "printer")
  496. s = ""
  497. try:
  498. rd = json.loads(r)
  499. if ("state" in rd) and ("text" in rd["state"]):
  500. s += rd["state"]["text"]
  501. if "temperature" in rd:
  502. s += " - "
  503. if "temperature" in rd:
  504. if "bed" in rd["temperature"]:
  505. if "actual" in rd["temperature"]["bed"]:
  506. s += "B"
  507. s += "%.1f" % rd["temperature"]["bed"]["actual"]
  508. if "target" in rd["temperature"]["bed"]:
  509. s += "/"
  510. s += "%.1f" % rd["temperature"]["bed"]["target"]
  511. s += " "
  512. if "tool0" in rd["temperature"]:
  513. if "actual" in rd["temperature"]["tool0"]:
  514. s += "T"
  515. s += "%.1f" % rd["temperature"]["tool0"]["actual"]
  516. if "target" in rd["temperature"]["tool0"]:
  517. s += "/"
  518. s += "%.1f" % rd["temperature"]["tool0"]["target"]
  519. s += " "
  520. if "tool1" in rd["temperature"]:
  521. if "actual" in rd["temperature"]["tool1"]:
  522. s += "T"
  523. s += "%.1f" % rd["temperature"]["tool1"]["actual"]
  524. if "target" in rd["temperature"]["tool1"]:
  525. s += "/"
  526. s += "%.1f" % rd["temperature"]["tool1"]["target"]
  527. s += " "
  528. except json.JSONDecodeError:
  529. pass
  530. return s.strip()
  531. def getState(self, host, key):
  532. r = self.sendGetRequest(host, key, "job")
  533. try:
  534. rd = json.loads(r)
  535. if "state" in rd:
  536. return rd["state"]
  537. except json.JSONDecodeError:
  538. pass
  539. return "Unknown"
  540. def getProgress(self, host, key):
  541. r = self.sendGetRequest(host, key, "job")
  542. try:
  543. rd = json.loads(r)
  544. if "progress" in rd:
  545. return rd["progress"]
  546. except json.JSONDecodeError:
  547. pass
  548. return "Unknown"
  549. def getName(self, host, key):
  550. r = self.sendGetRequest(host, key, "printerprofiles")
  551. try:
  552. rd = json.loads(r)
  553. if "profiles" in rd:
  554. p = next(iter(rd["profiles"]))
  555. if "name" in rd["profiles"][p]:
  556. return rd["profiles"][p]["name"]
  557. except json.JSONDecodeError:
  558. pass
  559. return host
  560. def getMethod(self, host, key):
  561. r = self.sendGetRequest(host, key, "plugin/psucontrol")
  562. if r == "timeout":
  563. return "unknown"
  564. try:
  565. rd = json.loads(r)
  566. if "isPSUOn" in rd:
  567. return "psucontrol"
  568. except json.JSONDecodeError:
  569. pass
  570. r = self.sendGetRequest(host, key, "system/commands/custom")
  571. if r == "timeout":
  572. return "unknown"
  573. try:
  574. rd = json.loads(r)
  575. for c in rd:
  576. if "action" in c:
  577. # we have some custom commands and no psucontrol
  578. # so lets try to use that instead of skipping
  579. # the printer completely with 'unknown'
  580. return "system"
  581. except json.JSONDecodeError:
  582. pass
  583. return "unknown"
  584. def getSystemCommands(self, host, key):
  585. l = []
  586. r = self.sendGetRequest(host, key, "system/commands/custom")
  587. try:
  588. rd = json.loads(r)
  589. if len(rd) > 0:
  590. print("system commands available for " + host + ":")
  591. for c in rd:
  592. if "action" in c:
  593. print(" - " + c["action"])
  594. l.append(c["action"])
  595. except json.JSONDecodeError:
  596. pass
  597. return l
  598. def setPSUControl(self, host, key, state):
  599. cmd = "turnPSUOff"
  600. if state:
  601. cmd = "turnPSUOn"
  602. return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
  603. def setSystemCommand(self, host, key, cmd):
  604. cmd = urllib.parse.quote(cmd)
  605. return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
  606. def exit(self):
  607. QCoreApplication.quit()
  608. def printerSystemCommandAction(self, item, index):
  609. if "off" in item[2][index].lower():
  610. state = self.getState(item[0], item[1])
  611. if state in self.statesWithWarning:
  612. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to run '" + item[2][index] + "'?", True, True) == False:
  613. return
  614. safe = self.getTemperatureIsSafe(item[0], item[1])
  615. if safe == False:
  616. if self.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to turn it off?", True, True) == False:
  617. return
  618. self.setSystemCommand(item[0], item[1], item[2][index])
  619. def printerOnAction(self, item):
  620. self.setPSUControl(item[0], item[1], True)
  621. def printerOffAction(self, item):
  622. state = self.getState(item[0], item[1])
  623. if state in self.statesWithWarning:
  624. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == False:
  625. return
  626. safe = self.getTemperatureIsSafe(item[0], item[1])
  627. if safe == False:
  628. if self.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to turn it off?", True, True) == False:
  629. return
  630. self.setPSUControl(item[0], item[1], False)
  631. def printerWebAction(self, item):
  632. self.openBrowser(item[0])
  633. def printerStatusAction(self, item):
  634. progress = self.getProgress(item[0], item[1])
  635. s = item[0] + "\n"
  636. warning = False
  637. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  638. s += "%.1f%% Completion\n" % progress["completion"]
  639. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  640. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  641. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  642. s += "No job is currently running"
  643. else:
  644. s += "Could not read printer status!"
  645. warning = True
  646. t = self.getTemperatureString(item[0], item[1])
  647. if len(t) > 0:
  648. s += "\n" + t
  649. self.showDialog("OctoTray Status", s, None, False, warning)
  650. def setTemperature(self, host, key, what, temp):
  651. path = "printer/bed"
  652. s = "{\"command\": \"target\", \"target\": " + temp + "}"
  653. if "tool" in what:
  654. path = "printer/tool"
  655. s = "{\"command\": \"target\", \"targets\": {\"" + what + "\": " + temp + "}}"
  656. if temp == None:
  657. temp = 0
  658. self.sendPostRequest(host, key, path, s)
  659. def printerHeatTool(self, p):
  660. self.setTemperature(p[0], p[1], "tool0", p[2])
  661. def printerHeatBed(self, p):
  662. self.setTemperature(p[0], p[1], "bed", p[3])
  663. def printerCooldown(self, p):
  664. state = self.getState(p[0], p[1])
  665. if state in self.statesWithWarning:
  666. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == False:
  667. return
  668. self.setTemperature(p[0], p[1], "tool0", 0)
  669. self.setTemperature(p[0], p[1], "bed", 0)
  670. def printerWebcamAction(self, item):
  671. for cw in self.camWindows:
  672. if cw.getHost() == item[0]:
  673. cw.show()
  674. cw.activateWindow()
  675. return
  676. window = CamWindow(self, item)
  677. self.camWindows.append(window)
  678. window.show()
  679. window.activateWindow()
  680. screenGeometry = QDesktopWidget().screenGeometry()
  681. x = (screenGeometry.width() - window.width()) / 2
  682. y = (screenGeometry.height() - window.height()) / 2
  683. x += screenGeometry.x()
  684. y += screenGeometry.y()
  685. window.setGeometry(int(x), int(y), int(window.width()), int(window.height()))
  686. def removeWebcamWindow(self, window):
  687. self.camWindows.remove(window)
  688. def showSettingsAction(self):
  689. if self.settingsWindow != None:
  690. self.settingsWindow.show()
  691. self.settingsWindow.activateWindow()
  692. return
  693. self.settingsWindow = SettingsWindow(self)
  694. self.settingsWindow.show()
  695. self.settingsWindow.activateWindow()
  696. screenGeometry = QDesktopWidget().screenGeometry()
  697. x = (screenGeometry.width() - self.settingsWindow.width()) / 2
  698. y = (screenGeometry.height() - self.settingsWindow.height()) / 2
  699. x += screenGeometry.x()
  700. y += screenGeometry.y()
  701. self.settingsWindow.setGeometry(int(x), int(y), int(self.settingsWindow.width()), int(self.settingsWindow.height()) + 50)
  702. def removeSettingsWindow(self):
  703. self.settingsWindow = None
  704. def restartApp(self):
  705. QCoreApplication.exit(42)
  706. def closeAll(self):
  707. for cw in self.camWindows:
  708. cw.close()
  709. if self.settingsWindow != None:
  710. self.settingsWindow.close()
  711. self.trayIcon.setVisible(False)
  712. if __name__ == "__main__":
  713. app = QtWidgets.QApplication(sys.argv)
  714. tray = OctoTray(app)
  715. rc = app.exec_()
  716. while rc == 42:
  717. tray.closeAll()
  718. tray = OctoTray(app)
  719. rc = app.exec_()
  720. sys.exit(rc)