Переглянути джерело

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 роки тому
джерело
коміт
fb497e0f3a
3 змінених файлів з 162 додано та 48 видалено
  1. 1
    1
      PKGBUILD
  2. 1
    1
      README.md
  3. 160
    46
      octotray

+ 1
- 1
PKGBUILD Переглянути файл

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

+ 1
- 1
README.md Переглянути файл

3
 Simple Python Qt Linux client for OctoPrint. Install on Arch Linux like this:
3
 Simple Python Qt Linux client for OctoPrint. Install on Arch Linux like this:
4
 
4
 
5
     makepkg
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
 Or on all other linux distros:
8
 Or on all other linux distros:
9
 
9
 

+ 160
- 46
octotray Переглянути файл

17
 import os
17
 import os
18
 import threading
18
 import threading
19
 import time
19
 import time
20
+import urllib.parse
20
 from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
21
 from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
21
 from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout
22
 from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout
22
 from PyQt5.QtGui import QIcon, QPixmap, QImageReader
23
 from PyQt5.QtGui import QIcon, QPixmap, QImageReader
52
 
53
 
53
 class CamWindow(QWidget):
54
 class CamWindow(QWidget):
54
     reloadDelayDefault = 1000 # in ms
55
     reloadDelayDefault = 1000 # in ms
56
+    statusDelay = 10 * 1000 # in ms
55
     addSize = 100
57
     addSize = 100
56
     reloadOn = True
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
         super(CamWindow, self).__init__(*args, **kwargs)
61
         super(CamWindow, self).__init__(*args, **kwargs)
60
-        self.url = "http://" + host + ":8080/?action=snapshot"
61
-        self.host = host
62
         self.app = app
62
         self.app = app
63
-        self.parent = parent
64
         self.manager = manager
63
         self.manager = manager
65
         self.manager.finished.connect(self.handleResponse)
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
         self.setWindowTitle(name + " Webcam Stream")
70
         self.setWindowTitle(name + " Webcam Stream")
68
         self.setWindowIcon(icon)
71
         self.setWindowIcon(icon)
95
         self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
98
         self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
96
         slide.addWidget(self.slideLabel, 0)
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
         size = self.size()
105
         size = self.size()
99
         size.setHeight(size.height() + self.addSize)
106
         size.setHeight(size.height() + self.addSize)
100
         self.resize(size)
107
         self.resize(size)
101
 
108
 
102
         self.loadImage()
109
         self.loadImage()
110
+        self.loadStatus()
103
 
111
 
104
     def getHost(self):
112
     def getHost(self):
105
         return self.host
113
         return self.host
112
         self.url = ""
120
         self.url = ""
113
         self.parent.removeWebcamWindow(self)
121
         self.parent.removeWebcamWindow(self)
114
 
122
 
115
-    def scheduleLoad(self):
123
+    def scheduleLoadImage(self):
116
         if self.reloadOn:
124
         if self.reloadOn:
117
             QTimer.singleShot(self.slider.value(), self.loadImage)
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
     def loadImage(self):
131
     def loadImage(self):
120
         url = QUrl(self.url)
132
         url = QUrl(self.url)
121
         request = QtNetwork.QNetworkRequest(url)
133
         request = QtNetwork.QNetworkRequest(url)
122
         self.manager.get(request)
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
     def handleResponse(self, reply):
155
     def handleResponse(self, reply):
125
         if reply.url().url() == self.url:
156
         if reply.url().url() == self.url:
126
             if reply.error() == QtNetwork.QNetworkReply.NoError:
157
             if reply.error() == QtNetwork.QNetworkReply.NoError:
131
                     if image.colorSpace().isValid():
162
                     if image.colorSpace().isValid():
132
                         image.convertToColorSpace(QColorSpace.SRgb)
163
                         image.convertToColorSpace(QColorSpace.SRgb)
133
                     self.img.setPixmap(QPixmap.fromImage(image))
164
                     self.img.setPixmap(QPixmap.fromImage(image))
134
-                    self.scheduleLoad()
165
+                    self.scheduleLoadImage()
135
                 else:
166
                 else:
136
                     print("Error decoding image: " + reader.errorString())
167
                     print("Error decoding image: " + reader.errorString())
137
             else:
168
             else:
146
     iconName = "octotray_icon.png"
177
     iconName = "octotray_icon.png"
147
 
178
 
148
     # 0=host, 1=key
179
     # 0=host, 1=key
149
-    # (2=method, 3=menu, 4...=actions)
180
+    # (2=system-commands, 3=menu, 4...=actions)
150
     printers = [
181
     printers = [
151
         [ "PRINTER_HOST_HERE", "PRINTER_API_KEY_HERE" ]
182
         [ "PRINTER_HOST_HERE", "PRINTER_API_KEY_HERE" ]
152
     ]
183
     ]
162
         QCoreApplication.setApplicationName(self.name)
193
         QCoreApplication.setApplicationName(self.name)
163
 
194
 
164
         if not QSystemTrayIcon.isSystemTrayAvailable():
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
             sys.exit(0)
197
             sys.exit(0)
167
 
198
 
168
         self.manager = QtNetwork.QNetworkAccessManager()
199
         self.manager = QtNetwork.QNetworkAccessManager()
169
-
170
         self.menu = QMenu()
200
         self.menu = QMenu()
171
 
201
 
202
+        unknownCount = 0
172
         for p in self.printers:
203
         for p in self.printers:
173
             method = self.getMethod(p[0], p[1])
204
             method = self.getMethod(p[0], p[1])
174
             print("Printer " + p[0] + " has method " + method)
205
             print("Printer " + p[0] + " has method " + method)
175
-            p.append(method)
176
             if method == "unknown":
206
             if method == "unknown":
207
+                unknownCount += 1
177
                 continue
208
                 continue
178
 
209
 
210
+            commands = self.getSystemCommands(p[0], p[1])
211
+            p.append(commands)
212
+
179
             menu = QMenu(self.getName(p[0], p[1]))
213
             menu = QMenu(self.getName(p[0], p[1]))
180
             p.append(menu)
214
             p.append(menu)
181
             self.menu.addMenu(menu)
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
             action = QAction("Get Status")
234
             action = QAction("Get Status")
194
             action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
235
             action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
205
             p.append(action)
246
             p.append(action)
206
             menu.addAction(action)
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
         self.quitAction = QAction("&Quit")
253
         self.quitAction = QAction("&Quit")
209
         self.quitAction.triggered.connect(self.exit)
254
         self.quitAction.triggered.connect(self.exit)
210
         self.menu.addAction(self.quitAction)
255
         self.menu.addAction(self.quitAction)
215
         elif os.path.isfile(self.iconPath + self.iconName):
260
         elif os.path.isfile(self.iconPath + self.iconName):
216
             iconPathName = self.iconPath + self.iconName
261
             iconPathName = self.iconPath + self.iconName
217
         else:
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
         self.icon = QIcon()
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
         trayIcon = QSystemTrayIcon(self.icon)
271
         trayIcon = QSystemTrayIcon(self.icon)
227
         trayIcon.setToolTip(self.name + " " + self.version)
272
         trayIcon.setToolTip(self.name + " " + self.version)
233
     def openBrowser(self, url):
278
     def openBrowser(self, url):
234
         os.system("xdg-open http://" + url)
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
         msg = QMessageBox()
282
         msg = QMessageBox()
238
 
283
 
239
-        if warning:
284
+        if error:
285
+            msg.setIcon(QMessageBox.Critical)
286
+        elif warning:
240
             msg.setIcon(QMessageBox.Warning)
287
             msg.setIcon(QMessageBox.Warning)
288
+        elif question:
289
+            msg.setIcon(QMessageBox.Question)
241
         else:
290
         else:
242
             msg.setIcon(QMessageBox.Information)
291
             msg.setIcon(QMessageBox.Information)
243
 
292
 
255
         retval = msg.exec_()
304
         retval = msg.exec_()
256
         if retval == QMessageBox.Yes:
305
         if retval == QMessageBox.Yes:
257
             return True
306
             return True
258
-        return False
307
+        else:
308
+            return False
259
 
309
 
260
     def sendRequest(self, host, headers, path, content = None):
310
     def sendRequest(self, host, headers, path, content = None):
261
-        cmdline = 'curl -s'
311
+        cmdline = 'curl -s -m 1'
262
         for h in headers:
312
         for h in headers:
263
             cmdline += " -H \"" + h + "\""
313
             cmdline += " -H \"" + h + "\""
264
         if content == None:
314
         if content == None:
279
         headers = [ "X-Api-Key: " + key ]
329
         headers = [ "X-Api-Key: " + key ]
280
         return self.sendRequest(host, headers, path)
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
     def getState(self, host, key):
374
     def getState(self, host, key):
283
         r = self.sendGetRequest(host, key, "job")
375
         r = self.sendGetRequest(host, key, "job")
284
         try:
376
         try:
325
             rd = json.loads(r)
417
             rd = json.loads(r)
326
             for c in rd:
418
             for c in rd:
327
                 if "action" in c:
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
         except json.JSONDecodeError:
424
         except json.JSONDecodeError:
331
             pass
425
             pass
332
 
426
 
333
         return "unknown"
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
     def setPSUControl(self, host, key, state):
446
     def setPSUControl(self, host, key, state):
336
         cmd = "turnPSUOff"
447
         cmd = "turnPSUOff"
337
         if state:
448
         if state:
338
             cmd = "turnPSUOn"
449
             cmd = "turnPSUOn"
339
         return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
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
         return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
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
     def exit(self):
456
     def exit(self):
356
         QCoreApplication.quit()
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
     def printerOnAction(self, item):
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
     def printerOffAction(self, item):
472
     def printerOffAction(self, item):
362
         state = self.getState(item[0], item[1])
473
         state = self.getState(item[0], item[1])
363
         if state in self.statesWithWarning:
474
         if state in self.statesWithWarning:
364
             if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == True:
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
         else:
477
         else:
367
-            self.setPrinter(item[0], item[1], item[2], False)
478
+            self.setPSUControl(item[0], item[1], False)
368
 
479
 
369
     def printerWebAction(self, item):
480
     def printerWebAction(self, item):
370
         self.openBrowser(item[0])
481
         self.openBrowser(item[0])
376
         if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
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
             s = "%.1f%% Completion\n" % progress["completion"]
488
             s = "%.1f%% Completion\n" % progress["completion"]
378
             s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
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
         elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
491
         elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
381
             s = "No job is currently running"
492
             s = "No job is currently running"
382
         else:
493
         else:
383
             s = "Could not read printer status!"
494
             s = "Could not read printer status!"
384
             warning = True
495
             warning = True
496
+        t = self.getTemperatureString(item[0], item[1])
497
+        if len(t) > 0:
498
+            s += "\n" + t
385
         self.showDialog("OctoTray Status", s, None, False, warning)
499
         self.showDialog("OctoTray Status", s, None, False, warning)
386
 
500
 
387
     def printerWebcamAction(self, item):
501
     def printerWebcamAction(self, item):
391
                 cw.activateWindow()
505
                 cw.activateWindow()
392
                 return
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
         self.camWindows.append(window)
509
         self.camWindows.append(window)
396
 
510
 
397
         screenGeometry = QDesktopWidget().screenGeometry()
511
         screenGeometry = QDesktopWidget().screenGeometry()

Завантаження…
Відмінити
Зберегти