Browse Source

Lots of updates. Added Temperature output to status dialog. Showing status in Webcam window. Abort if no printer available. Allow use of all system commands.

Thomas Buck 3 years ago
parent
commit
fb497e0f3a
3 changed files with 162 additions and 48 deletions
  1. 1
    1
      PKGBUILD
  2. 1
    1
      README.md
  3. 160
    46
      octotray

+ 1
- 1
PKGBUILD View File

@@ -1,6 +1,6 @@
1 1
 # Maintainer: Thomas Buck <thomas@xythobuz.de>
2 2
 pkgname=OctoTray
3
-pkgver=0.1
3
+pkgver=0.2
4 4
 pkgrel=1
5 5
 pkgdesc="Control OctoPrint instances from system tray"
6 6
 arch=('any')

+ 1
- 1
README.md View File

@@ -3,7 +3,7 @@
3 3
 Simple Python Qt Linux client for OctoPrint. Install on Arch Linux like this:
4 4
 
5 5
     makepkg
6
-    sudo pacman -U octotray-0.1-1-any.pkg.tar.xz
6
+    sudo pacman -U octotray-0.2-1-any.pkg.tar.xz
7 7
 
8 8
 Or on all other linux distros:
9 9
 

+ 160
- 46
octotray View File

@@ -17,6 +17,7 @@ import sys
17 17
 import os
18 18
 import threading
19 19
 import time
20
+import urllib.parse
20 21
 from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
21 22
 from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout
22 23
 from PyQt5.QtGui import QIcon, QPixmap, QImageReader
@@ -52,17 +53,19 @@ class AspectRatioPixmapLabel(QLabel):
52 53
 
53 54
 class CamWindow(QWidget):
54 55
     reloadDelayDefault = 1000 # in ms
56
+    statusDelay = 10 * 1000 # in ms
55 57
     addSize = 100
56 58
     reloadOn = True
57 59
 
58
-    def __init__(self, parent, name, icon, app, manager, host, *args, **kwargs):
60
+    def __init__(self, parent, name, icon, app, manager, printer, *args, **kwargs):
59 61
         super(CamWindow, self).__init__(*args, **kwargs)
60
-        self.url = "http://" + host + ":8080/?action=snapshot"
61
-        self.host = host
62 62
         self.app = app
63
-        self.parent = parent
64 63
         self.manager = manager
65 64
         self.manager.finished.connect(self.handleResponse)
65
+        self.parent = parent
66
+        self.printer = printer
67
+        self.host = self.printer[0]
68
+        self.url = "http://" + self.host + ":8080/?action=snapshot"
66 69
 
67 70
         self.setWindowTitle(name + " Webcam Stream")
68 71
         self.setWindowIcon(icon)
@@ -95,11 +98,16 @@ class CamWindow(QWidget):
95 98
         self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
96 99
         slide.addWidget(self.slideLabel, 0)
97 100
 
101
+        self.statusLabel = QLabel("Status: unavailable")
102
+        box.addWidget(self.statusLabel, 0)
103
+        box.setAlignment(label, Qt.AlignHCenter)
104
+
98 105
         size = self.size()
99 106
         size.setHeight(size.height() + self.addSize)
100 107
         self.resize(size)
101 108
 
102 109
         self.loadImage()
110
+        self.loadStatus()
103 111
 
104 112
     def getHost(self):
105 113
         return self.host
@@ -112,15 +120,38 @@ class CamWindow(QWidget):
112 120
         self.url = ""
113 121
         self.parent.removeWebcamWindow(self)
114 122
 
115
-    def scheduleLoad(self):
123
+    def scheduleLoadImage(self):
116 124
         if self.reloadOn:
117 125
             QTimer.singleShot(self.slider.value(), self.loadImage)
118 126
 
127
+    def scheduleLoadStatus(self):
128
+        if self.reloadOn:
129
+            QTimer.singleShot(self.statusDelay, self.loadStatus)
130
+
119 131
     def loadImage(self):
120 132
         url = QUrl(self.url)
121 133
         request = QtNetwork.QNetworkRequest(url)
122 134
         self.manager.get(request)
123 135
 
136
+    def loadStatus(self):
137
+        s = "Status: "
138
+        t = self.parent.getTemperatureString(self.host, self.printer[1])
139
+        if len(t) > 0:
140
+            s += t
141
+        else:
142
+            s += "Unknown"
143
+
144
+        progress = self.parent.getProgress(self.host, self.printer[1])
145
+        if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
146
+            s += " - %.1f%%" % progress["completion"]
147
+            s += " - runtime "
148
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
149
+            s += " - "
150
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
151
+
152
+        self.statusLabel.setText(s)
153
+        self.scheduleLoadStatus()
154
+
124 155
     def handleResponse(self, reply):
125 156
         if reply.url().url() == self.url:
126 157
             if reply.error() == QtNetwork.QNetworkReply.NoError:
@@ -131,7 +162,7 @@ class CamWindow(QWidget):
131 162
                     if image.colorSpace().isValid():
132 163
                         image.convertToColorSpace(QColorSpace.SRgb)
133 164
                     self.img.setPixmap(QPixmap.fromImage(image))
134
-                    self.scheduleLoad()
165
+                    self.scheduleLoadImage()
135 166
                 else:
136 167
                     print("Error decoding image: " + reader.errorString())
137 168
             else:
@@ -146,7 +177,7 @@ class OctoTray():
146 177
     iconName = "octotray_icon.png"
147 178
 
148 179
     # 0=host, 1=key
149
-    # (2=method, 3=menu, 4...=actions)
180
+    # (2=system-commands, 3=menu, 4...=actions)
150 181
     printers = [
151 182
         [ "PRINTER_HOST_HERE", "PRINTER_API_KEY_HERE" ]
152 183
     ]
@@ -162,33 +193,43 @@ class OctoTray():
162 193
         QCoreApplication.setApplicationName(self.name)
163 194
 
164 195
         if not QSystemTrayIcon.isSystemTrayAvailable():
165
-            print("System Tray is not available on this platform!")
196
+            self.showDialog("OctoTray Error", "System Tray is not available on this platform!", "", False, False, True)
166 197
             sys.exit(0)
167 198
 
168 199
         self.manager = QtNetwork.QNetworkAccessManager()
169
-
170 200
         self.menu = QMenu()
171 201
 
202
+        unknownCount = 0
172 203
         for p in self.printers:
173 204
             method = self.getMethod(p[0], p[1])
174 205
             print("Printer " + p[0] + " has method " + method)
175
-            p.append(method)
176 206
             if method == "unknown":
207
+                unknownCount += 1
177 208
                 continue
178 209
 
210
+            commands = self.getSystemCommands(p[0], p[1])
211
+            p.append(commands)
212
+
179 213
             menu = QMenu(self.getName(p[0], p[1]))
180 214
             p.append(menu)
181 215
             self.menu.addMenu(menu)
182 216
 
183
-            action = QAction("Turn on")
184
-            action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
185
-            p.append(action)
186
-            menu.addAction(action)
217
+            if method == "psucontrol":
218
+                action = QAction("Turn On PSU")
219
+                action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
220
+                p.append(action)
221
+                menu.addAction(action)
187 222
 
188
-            action = QAction("Turn off")
189
-            action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
190
-            p.append(action)
191
-            menu.addAction(action)
223
+                action = QAction("Turn Off PSU")
224
+                action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
225
+                p.append(action)
226
+                menu.addAction(action)
227
+
228
+            for i in range(0, len(commands)):
229
+                action = QAction(commands[i].title())
230
+                action.triggered.connect(lambda chk, x=p, y=i: self.printerSystemCommandAction(x, y))
231
+                p.append(action)
232
+                menu.addAction(action)
192 233
 
193 234
             action = QAction("Get Status")
194 235
             action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
@@ -205,6 +246,10 @@ class OctoTray():
205 246
             p.append(action)
206 247
             menu.addAction(action)
207 248
 
249
+        if (len(self.printers) <= 0) or (unknownCount >= len(self.printers)):
250
+            self.showDialog("OctoTray Error", "No printers available!", "", False, False, True)
251
+            sys.exit(0)
252
+
208 253
         self.quitAction = QAction("&Quit")
209 254
         self.quitAction.triggered.connect(self.exit)
210 255
         self.menu.addAction(self.quitAction)
@@ -215,13 +260,13 @@ class OctoTray():
215 260
         elif os.path.isfile(self.iconPath + self.iconName):
216 261
             iconPathName = self.iconPath + self.iconName
217 262
         else:
218
-            print("no icon found")
263
+            self.showDialog("OctoTray Error", "Icon file has not been found! found", "", False, False, True)
264
+            sys.exit(0)
219 265
 
220 266
         self.icon = QIcon()
221
-        if iconPathName != "":
222
-            pic = QPixmap(32, 32)
223
-            pic.load(iconPathName)
224
-            self.icon = QIcon(pic)
267
+        pic = QPixmap(32, 32)
268
+        pic.load(iconPathName)
269
+        self.icon = QIcon(pic)
225 270
 
226 271
         trayIcon = QSystemTrayIcon(self.icon)
227 272
         trayIcon.setToolTip(self.name + " " + self.version)
@@ -233,11 +278,15 @@ class OctoTray():
233 278
     def openBrowser(self, url):
234 279
         os.system("xdg-open http://" + url)
235 280
 
236
-    def showDialog(self, title, text1, text2, question, warning):
281
+    def showDialog(self, title, text1, text2 = "", question = False, warning = False, error = False):
237 282
         msg = QMessageBox()
238 283
 
239
-        if warning:
284
+        if error:
285
+            msg.setIcon(QMessageBox.Critical)
286
+        elif warning:
240 287
             msg.setIcon(QMessageBox.Warning)
288
+        elif question:
289
+            msg.setIcon(QMessageBox.Question)
241 290
         else:
242 291
             msg.setIcon(QMessageBox.Information)
243 292
 
@@ -255,10 +304,11 @@ class OctoTray():
255 304
         retval = msg.exec_()
256 305
         if retval == QMessageBox.Yes:
257 306
             return True
258
-        return False
307
+        else:
308
+            return False
259 309
 
260 310
     def sendRequest(self, host, headers, path, content = None):
261
-        cmdline = 'curl -s'
311
+        cmdline = 'curl -s -m 1'
262 312
         for h in headers:
263 313
             cmdline += " -H \"" + h + "\""
264 314
         if content == None:
@@ -279,6 +329,48 @@ class OctoTray():
279 329
         headers = [ "X-Api-Key: " + key ]
280 330
         return self.sendRequest(host, headers, path)
281 331
 
332
+    def getTemperatureString(self, host, key):
333
+        r = self.sendGetRequest(host, key, "printer")
334
+        s = ""
335
+        try:
336
+            rd = json.loads(r)
337
+
338
+            if ("state" in rd) and ("text" in rd["state"]):
339
+                s += rd["state"]["text"]
340
+                if "temperature" in rd:
341
+                    s += " - "
342
+
343
+            if "temperature" in rd:
344
+                if "bed" in rd["temperature"]:
345
+                    if "actual" in rd["temperature"]["bed"]:
346
+                        s += "B"
347
+                        s += "%.1f" % rd["temperature"]["bed"]["actual"]
348
+                        if "target" in rd["temperature"]["bed"]:
349
+                            s += "/"
350
+                            s += "%.1f" % rd["temperature"]["bed"]["target"]
351
+                        s += " "
352
+
353
+                if "tool0" in rd["temperature"]:
354
+                    if "actual" in rd["temperature"]["tool0"]:
355
+                        s += "T"
356
+                        s += "%.1f" % rd["temperature"]["tool0"]["actual"]
357
+                        if "target" in rd["temperature"]["tool0"]:
358
+                            s += "/"
359
+                            s += "%.1f" % rd["temperature"]["tool0"]["target"]
360
+                        s += " "
361
+
362
+                if "tool1" in rd["temperature"]:
363
+                    if "actual" in rd["temperature"]["tool1"]:
364
+                        s += "T"
365
+                        s += "%.1f" % rd["temperature"]["tool1"]["actual"]
366
+                        if "target" in rd["temperature"]["tool1"]:
367
+                            s += "/"
368
+                            s += "%.1f" % rd["temperature"]["tool1"]["target"]
369
+                        s += " "
370
+        except json.JSONDecodeError:
371
+            pass
372
+        return s.strip()
373
+
282 374
     def getState(self, host, key):
283 375
         r = self.sendGetRequest(host, key, "job")
284 376
         try:
@@ -325,46 +417,65 @@ class OctoTray():
325 417
             rd = json.loads(r)
326 418
             for c in rd:
327 419
                 if "action" in c:
328
-                    if (c["action"] == "all off") or (c["action"] == "all on"):
329
-                        return "system"
420
+                    # we have some custom commands and no psucontrol
421
+                    # so lets try to use that instead of skipping
422
+                    # the printer completely with 'unknown'
423
+                    return "system"
330 424
         except json.JSONDecodeError:
331 425
             pass
332 426
 
333 427
         return "unknown"
334 428
 
429
+    def getSystemCommands(self, host, key):
430
+        l = []
431
+        r = self.sendGetRequest(host, key, "system/commands/custom")
432
+        try:
433
+            rd = json.loads(r)
434
+
435
+            if len(rd) > 0:
436
+                print("system commands available for " + host + ":")
437
+
438
+            for c in rd:
439
+                if "action" in c:
440
+                    print("  - " + c["action"])
441
+                    l.append(c["action"])
442
+        except json.JSONDecodeError:
443
+            pass
444
+        return l
445
+
335 446
     def setPSUControl(self, host, key, state):
336 447
         cmd = "turnPSUOff"
337 448
         if state:
338 449
             cmd = "turnPSUOn"
339 450
         return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
340 451
 
341
-    def setSystemCommand(self, host, key, state):
342
-        cmd = "all%20off"
343
-        if state:
344
-            cmd = "all%20on"
452
+    def setSystemCommand(self, host, key, cmd):
453
+        cmd = urllib.parse.quote(cmd)
345 454
         return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
346 455
 
347
-    def setPrinter(self, host, key, method, state):
348
-        if method == "psucontrol":
349
-            return self.setPSUControl(host, key, state)
350
-        elif method == "system":
351
-            return self.setSystemCommand(host, key, state)
352
-        else:
353
-            return "error"
354
-
355 456
     def exit(self):
356 457
         QCoreApplication.quit()
357 458
 
459
+    def printerSystemCommandAction(self, item, index):
460
+        if "off" in item[2][index].lower():
461
+            state = self.getState(item[0], item[1])
462
+            if state in self.statesWithWarning:
463
+                if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to run '" + item[2][index] + "'?", True, True) == True:
464
+                    self.setSystemCommand(item[0], item[1], item[2][index])
465
+                else:
466
+                    return
467
+        self.setSystemCommand(item[0], item[1], item[2][index])
468
+
358 469
     def printerOnAction(self, item):
359
-        self.setPrinter(item[0], item[1], item[2], True)
470
+        self.setPSUControl(item[0], item[1], True)
360 471
 
361 472
     def printerOffAction(self, item):
362 473
         state = self.getState(item[0], item[1])
363 474
         if state in self.statesWithWarning:
364 475
             if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == True:
365
-                self.setPrinter(item[0], item[1], item[2], False)
476
+                self.setPSUControl(item[0], item[1], False)
366 477
         else:
367
-            self.setPrinter(item[0], item[1], item[2], False)
478
+            self.setPSUControl(item[0], item[1], False)
368 479
 
369 480
     def printerWebAction(self, item):
370 481
         self.openBrowser(item[0])
@@ -376,12 +487,15 @@ class OctoTray():
376 487
         if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
377 488
             s = "%.1f%% Completion\n" % progress["completion"]
378 489
             s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
379
-            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left\n"
490
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
380 491
         elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
381 492
             s = "No job is currently running"
382 493
         else:
383 494
             s = "Could not read printer status!"
384 495
             warning = True
496
+        t = self.getTemperatureString(item[0], item[1])
497
+        if len(t) > 0:
498
+            s += "\n" + t
385 499
         self.showDialog("OctoTray Status", s, None, False, warning)
386 500
 
387 501
     def printerWebcamAction(self, item):
@@ -391,7 +505,7 @@ class OctoTray():
391 505
                 cw.activateWindow()
392 506
                 return
393 507
 
394
-        window = CamWindow(self, self.name, self.icon, self.app, self.manager, item[0])
508
+        window = CamWindow(self, self.name, self.icon, self.app, self.manager, item)
395 509
         self.camWindows.append(window)
396 510
 
397 511
         screenGeometry = QDesktopWidget().screenGeometry()

Loading…
Cancel
Save