Linux PyQt tray application to control OctoPrint instances
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

APIOctoprint.py 13KB

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)