|
@@ -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()
|