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 14KB

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