Linux PyQt tray application to control OctoPrint instances
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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"