Linux PyQt tray application to control OctoPrint instances
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # APIOctoprint.py
  5. #
  6. # HTTP API for OctoPrint.
  7. import json
  8. import time
  9. import urllib.parse
  10. import urllib.request
  11. import operator
  12. import socket
  13. class APIOctoprint():
  14. statesWithWarning = [
  15. "printing", "pausing", "paused"
  16. ]
  17. def __init__(self, parent, host, key):
  18. self.parent = parent
  19. self.host = host
  20. self.key = key
  21. # return list of tuples ( "name", func(name) )
  22. # with all available commands.
  23. # call function in with name of action!
  24. def getAvailableCommands(self):
  25. self.method = self.getMethod()
  26. print("Printer " + self.host + " has method " + self.method)
  27. commands = []
  28. if self.method == "unknown":
  29. # nothing available
  30. return commands
  31. # always add available system commands
  32. systemCommands = self.getSystemCommands()
  33. for sc in systemCommands:
  34. commands.append((sc, self.callSystemCommand))
  35. if self.method == "psucontrol":
  36. # support for psucontrol plugin
  37. commands.append(("Turn On PSU", self.setPower))
  38. commands.append(("Turn Off PSU", self.setPower))
  39. return commands
  40. ############
  41. # HTTP API #
  42. ############
  43. def sendRequest(self, headers, path, content = None):
  44. url = "http://" + self.host + "/api/" + path
  45. if content == None:
  46. request = urllib.request.Request(url, None, headers)
  47. else:
  48. data = content.encode('ascii')
  49. request = urllib.request.Request(url, data, headers)
  50. try:
  51. with urllib.request.urlopen(request, None, self.parent.networkTimeout) as response:
  52. text = response.read()
  53. return text
  54. except (urllib.error.URLError, urllib.error.HTTPError) as error:
  55. print("Error requesting URL \"" + url + "\": \"" + str(error) + "\"")
  56. return "error"
  57. except socket.timeout:
  58. print("Timeout waiting for response to \"" + url + "\"")
  59. return "timeout"
  60. def sendPostRequest(self, path, content):
  61. headers = {
  62. "Content-Type": "application/json",
  63. "X-Api-Key": self.key
  64. }
  65. return self.sendRequest(headers, path, content)
  66. def sendGetRequest(self, path):
  67. headers = {
  68. "X-Api-Key": self.key
  69. }
  70. return self.sendRequest(headers, path)
  71. #####################
  72. # Command discovery #
  73. #####################
  74. def getMethod(self):
  75. r = self.sendGetRequest("plugin/psucontrol")
  76. if r == "timeout":
  77. return "unknown"
  78. try:
  79. rd = json.loads(r)
  80. if "isPSUOn" in rd:
  81. return "psucontrol"
  82. except json.JSONDecodeError:
  83. pass
  84. r = self.sendGetRequest("system/commands/custom")
  85. if r == "timeout":
  86. return "unknown"
  87. try:
  88. rd = json.loads(r)
  89. for c in rd:
  90. if "action" in c:
  91. # we have some custom commands and no psucontrol
  92. # so lets try to use that instead of skipping
  93. # the printer completely with 'unknown'
  94. return "system"
  95. except json.JSONDecodeError:
  96. pass
  97. return "unknown"
  98. def getSystemCommands(self):
  99. l = []
  100. r = self.sendGetRequest("system/commands/custom")
  101. try:
  102. rd = json.loads(r)
  103. if len(rd) > 0:
  104. print("system commands available for " + self.host + ":")
  105. for c in rd:
  106. if "action" in c:
  107. print(" - " + c["action"])
  108. l.append(c["action"])
  109. except json.JSONDecodeError:
  110. pass
  111. return l
  112. #################
  113. # Safety Checks #
  114. #################
  115. def stateSafetyCheck(self, actionString):
  116. state = self.getState()
  117. if state.lower() in self.statesWithWarning:
  118. if self.parent.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to " + actionString + "?", True, True) == False:
  119. return True
  120. return False
  121. def tempSafetyCheck(self, actionString):
  122. if self.getTemperatureIsSafe() == False:
  123. if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
  124. return True
  125. return False
  126. def safetyCheck(self, actionString):
  127. if self.stateSafetyCheck(actionString):
  128. return True
  129. if self.tempSafetyCheck(actionString):
  130. return True
  131. return False
  132. ##################
  133. # Power Toggling #
  134. ##################
  135. def callSystemCommand(self, name):
  136. if "off" in name.lower():
  137. if self.safetyCheck("run '" + name + "'"):
  138. return
  139. cmd = urllib.parse.quote(name)
  140. self.sendPostRequest("system/commands/custom/" + cmd, '')
  141. def setPower(self, name):
  142. if "off" in name.lower():
  143. if self.safetyCheck(name):
  144. return
  145. cmd = "turnPSUOff"
  146. if "on" in name.lower():
  147. cmd = "turnPSUOn"
  148. return self.sendPostRequest("plugin/psucontrol", '{ "command":"' + cmd + '" }')
  149. def turnOn(self):
  150. if self.method == "psucontrol":
  151. self.setPower("on")
  152. elif self.method == "system":
  153. cmds = self.getSystemCommands()
  154. for cmd in cmds:
  155. if "on" in cmd:
  156. self.callSystemCommand(cmd)
  157. break
  158. def turnOff(self):
  159. if self.method == "psucontrol":
  160. self.setPower("off")
  161. elif self.method == "system":
  162. cmds = self.getSystemCommands()
  163. for cmd in cmds:
  164. if "off" in cmd:
  165. self.callSystemCommand(cmd)
  166. break
  167. ######################
  168. # Status Information #
  169. ######################
  170. def getTemperatureIsSafe(self, limit = 50.0):
  171. r = self.sendGetRequest("printer")
  172. try:
  173. rd = json.loads(r)
  174. if "temperature" in rd:
  175. if ("tool0" in rd["temperature"]) and ("actual" in rd["temperature"]["tool0"]):
  176. if rd["temperature"]["tool0"]["actual"] > limit:
  177. return False
  178. if ("tool1" in rd["temperature"]) and ("actual" in rd["temperature"]["tool1"]):
  179. if rd["temperature"]["tool1"]["actual"] > limit:
  180. return False
  181. except json.JSONDecodeError:
  182. pass
  183. return True
  184. def getTemperatureString(self):
  185. r = self.sendGetRequest("printer")
  186. s = ""
  187. try:
  188. rd = json.loads(r)
  189. except json.JSONDecodeError:
  190. return s
  191. if ("state" in rd) and ("text" in rd["state"]):
  192. s += rd["state"]["text"]
  193. if "temperature" in rd:
  194. s += " - "
  195. if "temperature" in rd:
  196. if ("bed" in rd["temperature"]) and ("actual" in rd["temperature"]["bed"]):
  197. s += "B"
  198. s += "%.1f" % rd["temperature"]["bed"]["actual"]
  199. if "target" in rd["temperature"]["bed"]:
  200. s += "/"
  201. s += "%.1f" % rd["temperature"]["bed"]["target"]
  202. s += " "
  203. if ("tool0" in rd["temperature"]) and ("actual" in rd["temperature"]["tool0"]):
  204. s += "T"
  205. s += "%.1f" % rd["temperature"]["tool0"]["actual"]
  206. if "target" in rd["temperature"]["tool0"]:
  207. s += "/"
  208. s += "%.1f" % rd["temperature"]["tool0"]["target"]
  209. s += " "
  210. if ("tool1" in rd["temperature"]) and ("actual" in rd["temperature"]["tool1"]):
  211. s += "T"
  212. s += "%.1f" % rd["temperature"]["tool1"]["actual"]
  213. if "target" in rd["temperature"]["tool1"]:
  214. s += "/"
  215. s += "%.1f" % rd["temperature"]["tool1"]["target"]
  216. s += " "
  217. return s.strip()
  218. def getState(self):
  219. r = self.sendGetRequest("job")
  220. try:
  221. rd = json.loads(r)
  222. if "state" in rd:
  223. return rd["state"]
  224. except json.JSONDecodeError:
  225. pass
  226. return "Unknown"
  227. def getProgress(self):
  228. r = self.sendGetRequest("job")
  229. try:
  230. rd = json.loads(r)
  231. if "progress" in rd:
  232. return rd["progress"]
  233. except json.JSONDecodeError:
  234. pass
  235. return "Unknown"
  236. def getName(self):
  237. r = self.sendGetRequest("printerprofiles")
  238. try:
  239. rd = json.loads(r)
  240. if "profiles" in rd:
  241. p = next(iter(rd["profiles"]))
  242. if "name" in rd["profiles"][p]:
  243. return rd["profiles"][p]["name"]
  244. except json.JSONDecodeError:
  245. pass
  246. return self.host
  247. def getProgressString(self):
  248. s = ""
  249. progress = self.getProgress()
  250. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  251. s += "%.1f%%" % progress["completion"]
  252. s += " - runtime "
  253. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
  254. s += " - "
  255. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  256. return s
  257. ###################
  258. # Printer Actions #
  259. ###################
  260. def callHoming(self, axes = "xyz"):
  261. if self.stateSafetyCheck("home it"):
  262. return
  263. axes_string = ''
  264. for i in range(0, len(axes)):
  265. axes_string += '"' + str(axes[i]) + '"'
  266. if i < (len(axes) - 1):
  267. axes_string += ', '
  268. self.sendPostRequest("printer/printhead", '{ "command": "home", "axes": [' + axes_string + '] }')
  269. def callMove(self, axis, dist, relative = True):
  270. if self.stateSafetyCheck("move it"):
  271. return
  272. absolute = ''
  273. if relative == False:
  274. absolute = ', "absolute": true'
  275. self.sendPostRequest("printer/printhead", '{ "command": "jog", "' + str(axis) + '": ' + str(dist) + ', "speed": ' + str(self.jogMoveSpeed) + absolute + ' }')
  276. def callPauseResume(self):
  277. if self.stateSafetyCheck("pause/resume"):
  278. return
  279. self.sendPostRequest("job", '{ "command": "pause", "action": "toggle" }')
  280. def callJobCancel(self):
  281. if self.stateSafetyCheck("cancel"):
  282. return
  283. self.sendPostRequest("job", '{ "command": "cancel" }')
  284. def statusDialog(self):
  285. progress = self.getProgress()
  286. s = self.host + "\n"
  287. warning = False
  288. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  289. s += "%.1f%% Completion\n" % progress["completion"]
  290. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  291. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  292. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  293. s += "No job is currently running"
  294. else:
  295. s += "Could not read printer status!"
  296. warning = True
  297. t = self.getTemperatureString()
  298. if len(t) > 0:
  299. s += "\n" + t
  300. self.parent.showDialog("OctoTray Status", s, None, False, warning)
  301. #################
  302. # File Handling #
  303. #################
  304. def getRecentFiles(self, count):
  305. r = self.sendGetRequest("files?recursive=true")
  306. files = []
  307. try:
  308. rd = json.loads(r)
  309. if "files" in rd:
  310. t = [f for f in rd["files"] if "date" in f]
  311. fs = sorted(t, key=operator.itemgetter("date"), reverse=True)
  312. for f in fs[:count]:
  313. files.append((f["name"], f["origin"] + "/" + f["path"]))
  314. except json.JSONDecodeError:
  315. pass
  316. return files
  317. def printFile(self, path):
  318. self.sendPostRequest("files/" + path, '{ "command": "select", "print": true }')
  319. ###############
  320. # Temperature #
  321. ###############
  322. def setTemperature(self, what, temp):
  323. path = "printer/bed"
  324. s = "{\"command\": \"target\", \"target\": " + temp + "}"
  325. if "tool" in what:
  326. path = "printer/tool"
  327. s = "{\"command\": \"target\", \"targets\": {\"" + what + "\": " + temp + "}}"
  328. if temp == None:
  329. temp = 0
  330. self.sendPostRequest(path, s)
  331. def printerHeatTool(self, temp):
  332. self.setTemperature("tool0", temp)
  333. def printerHeatBed(self, temp):
  334. self.setTemperature("bed", temp)
  335. def printerCooldown(self):
  336. if self.stateSafetyCheck("cool it down"):
  337. return
  338. self.setTemperature("tool0", 0)
  339. self.setTemperature("bed", 0)