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.

APIMoonraker.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # APIMoonraker.py
  5. #
  6. # HTTP API for Moonraker.
  7. import json
  8. import time
  9. import urllib.parse
  10. import urllib.request
  11. import operator
  12. import socket
  13. class APIMoonraker():
  14. # TODO are these states correct?
  15. statesWithWarning = [
  16. "printing", "pausing", "paused"
  17. ]
  18. def __init__(self, parent, host, webcam):
  19. self.parent = parent
  20. self.host = host
  21. self.webcamIndex = int(webcam)
  22. # return list of tuples ( "name", func(name) )
  23. # with all available commands.
  24. # call function with name of action!
  25. def getAvailableCommands(self):
  26. commands = []
  27. self.devices = self.getDeviceList()
  28. for d in self.devices:
  29. #for a in [ "Turn on", "Turn off", "Toggle" ]:
  30. for a in [ "Turn on", "Turn off" ]:
  31. name = a + " " + d
  32. cmd = ( name, self.toggleDevice )
  33. commands.append(cmd)
  34. return commands
  35. ############
  36. # HTTP API #
  37. ############
  38. # only used internally
  39. def sendRequest(self, headers, path, content = None):
  40. url = "http://" + self.host + "/" + path
  41. if content == None:
  42. request = urllib.request.Request(url, None, headers)
  43. else:
  44. data = content.encode('ascii')
  45. request = urllib.request.Request(url, data, headers)
  46. try:
  47. with urllib.request.urlopen(request, None, self.parent.networkTimeout) as response:
  48. text = response.read()
  49. #print("Klipper Rx: \"" + str(text) + "\"\n")
  50. return text
  51. except (urllib.error.URLError, urllib.error.HTTPError) as error:
  52. print("Error requesting URL \"" + url + "\": \"" + str(error) + "\"")
  53. return "error"
  54. except socket.timeout:
  55. print("Timeout waiting for response to \"" + url + "\"")
  56. return "timeout"
  57. # only used internally
  58. def sendPostRequest(self, path, content):
  59. headers = {
  60. "Content-Type": "application/json"
  61. }
  62. return self.sendRequest(headers, path, content)
  63. # only used internally
  64. def sendGetRequest(self, path):
  65. headers = {}
  66. return self.sendRequest(headers, path)
  67. #####################
  68. # Command discovery #
  69. #####################
  70. def getDeviceList(self):
  71. devices = []
  72. r = self.sendGetRequest("machine/device_power/devices")
  73. if (r == "timeout") or (r == "error"):
  74. return devices
  75. try:
  76. rd = json.loads(r)
  77. if "result" in rd:
  78. if "devices" in rd["result"]:
  79. for d in rd["result"]["devices"]:
  80. if "device" in d:
  81. devices.append(d["device"])
  82. except json.JSONDecodeError:
  83. pass
  84. return devices
  85. # return "unknown" when no power can be toggled
  86. def getMethod(self):
  87. if len(self.devices) <= 0:
  88. return "unknown"
  89. return "moonraker"
  90. #################
  91. # Safety Checks #
  92. #################
  93. # only used internally
  94. def stateSafetyCheck(self, actionString):
  95. state = self.getState()
  96. if state.lower() in self.statesWithWarning:
  97. if self.parent.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to " + actionString + "?", True, True) == False:
  98. return True
  99. return False
  100. # only used internally
  101. def tempSafetyCheck(self, actionString):
  102. if self.getTemperatureIsSafe() == False:
  103. if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
  104. return True
  105. return False
  106. # only used internally
  107. def safetyCheck(self, actionString):
  108. if self.stateSafetyCheck(actionString):
  109. return True
  110. if self.tempSafetyCheck(actionString):
  111. return True
  112. return False
  113. ##################
  114. # Power Toggling #
  115. ##################
  116. # only used internally (passed to caller as a pointer)
  117. def toggleDevice(self, name):
  118. # name is "Toggle x" or "Turn on x" or "Turn off x"
  119. action = ""
  120. if name.startswith("Toggle "):
  121. action = "toggle"
  122. name = name[len("Toggle "):]
  123. elif name.startswith("Turn on "):
  124. action = "on"
  125. name = name[len("Turn on "):]
  126. elif name.startswith("Turn off "):
  127. action = "off"
  128. name = name[len("Turn off "):]
  129. self.sendPostRequest("machine/device_power/device?device=" + name + "&action=" + action, "")
  130. # should automatically turn on printer, regardless of method
  131. def turnOn(self):
  132. if len(self.devices) > 0:
  133. self.toggleDevice("Turn on " + self.devices[0])
  134. # should automatically turn off printer, regardless of method
  135. def turnOff(self):
  136. if len(self.devices) > 0:
  137. self.toggleDevice("Turn off " + self.devices[0])
  138. ######################
  139. # Status Information #
  140. ######################
  141. # only used internally
  142. def getState(self):
  143. # just using octoprint compatibility layer
  144. r = self.sendGetRequest("api/job")
  145. try:
  146. rd = json.loads(r)
  147. if "state" in rd:
  148. return rd["state"]
  149. except json.JSONDecodeError:
  150. pass
  151. return "Unknown"
  152. # only used internally
  153. def getTemperatureIsSafe(self, limit = 50.0):
  154. self.sendGetRequest("printer/objects/query?extruder=temperature")
  155. if (r == "timeout") or (r == "error"):
  156. return files
  157. temp = 0.0
  158. try:
  159. rd = json.loads(r)
  160. if "result" in rd:
  161. if "status" in rd["result"]:
  162. if "extruder" in rd["result"]["status"]:
  163. if "temperature" in rd["result"]["status"]["extruder"]:
  164. temp = float(rd["result"]["status"]["extruder"]["temperature"])
  165. except json.JSONDecodeError:
  166. pass
  167. return temp < limit
  168. # human readable temperatures
  169. def getTemperatureString(self):
  170. r = self.sendGetRequest("printer/objects/query?extruder=temperature,target")
  171. s = "Unknown"
  172. try:
  173. rd = json.loads(r)
  174. if "result" in rd:
  175. if "status" in rd["result"]:
  176. if "extruder" in rd["result"]["status"]:
  177. temp = 0.0
  178. target = 0.0
  179. if "temperature" in rd["result"]["status"]["extruder"]:
  180. temp = float(rd["result"]["status"]["extruder"]["temperature"])
  181. if "target" in rd["result"]["status"]["extruder"]:
  182. target = float(rd["result"]["status"]["extruder"]["target"])
  183. s = str(temp) + " / " + str(target)
  184. except json.JSONDecodeError:
  185. pass
  186. return s
  187. # human readable name (fall back to hostname)
  188. def getName(self):
  189. r = self.sendGetRequest("printer/info")
  190. s = self.host
  191. try:
  192. rd = json.loads(r)
  193. if "result" in rd:
  194. if "hostname" in rd["result"]:
  195. s = rd["result"]["hostname"]
  196. except json.JSONDecodeError:
  197. pass
  198. return s
  199. # only used internally
  200. def getProgress(self):
  201. # just using octoprint compatibility layer
  202. r = self.sendGetRequest("api/job")
  203. try:
  204. rd = json.loads(r)
  205. if "progress" in rd:
  206. return rd["progress"]
  207. except json.JSONDecodeError:
  208. pass
  209. return "Unknown"
  210. # human readable progress
  211. def getProgressString(self):
  212. # just using octoprint compatibility layer
  213. s = ""
  214. progress = self.getProgress()
  215. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  216. s += "%.1f%%" % progress["completion"]
  217. s += " - runtime "
  218. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
  219. s += " - "
  220. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  221. return s
  222. ###################
  223. # Printer Actions #
  224. ###################
  225. # only used internally
  226. def sendGCode(self, cmd):
  227. self.sendPostRequest("printer/gcode/script?script=" + cmd, "")
  228. # only used internally
  229. def isPaused(self):
  230. r = self.sendGetRequest("objects/query?pause_resume")
  231. p = False
  232. try:
  233. rd = json.loads(r)
  234. if "result" in rd:
  235. if "status" in rd["result"]:
  236. if "pause_resume" in rd["result"]["status"]:
  237. if "is_paused" in rd["result"]["status"]["pause_resume"]:
  238. p = rd["result"]["status"]["pause_resume"]["is_paused"]
  239. except json.JSONDecodeError:
  240. pass
  241. return bool(p)
  242. # only used internally
  243. def isPositioningAbsolute(self):
  244. r = self.sendGetRequest("printer/objects/query?gcode_move=absolute_coordinates")
  245. p = True
  246. try:
  247. rd = json.loads(r)
  248. if "result" in rd:
  249. if "status" in rd["result"]:
  250. if "gcode_move" in rd["result"]["status"]:
  251. if "absolute_coordinates" in rd["result"]["status"]["gcode_move"]:
  252. p = rd["result"]["status"]["gcode_move"]["absolute_coordinates"]
  253. except json.JSONDecodeError:
  254. pass
  255. return bool(p)
  256. def callHoming(self, axes = "xyz"):
  257. if self.stateSafetyCheck("home it"):
  258. return
  259. # always home in XYZ order
  260. if "x" in axes:
  261. self.sendGCode("G28 X")
  262. if "y" in axes:
  263. self.sendGCode("G28 Y")
  264. if "z" in axes:
  265. self.sendGCode("G28 Z")
  266. def callMove(self, axis, dist, speed, relative = True):
  267. if self.stateSafetyCheck("move it"):
  268. return
  269. currentlyAbsolute = self.isPositioningAbsolute()
  270. if currentlyAbsolute and relative:
  271. # set to relative positioning
  272. self.sendGCode("G91")
  273. if (not currentlyAbsolute) and (not relative):
  274. # set to absolute positioning
  275. self.sendGCode("G90")
  276. # do move
  277. if axis.lower() == "x":
  278. self.sendGCode("G0 X" + str(dist) + " F" + str(speed))
  279. elif axis.lower() == "y":
  280. self.sendGCode("G0 Y" + str(dist) + " F" + str(speed))
  281. elif axis.lower() == "z":
  282. self.sendGCode("G0 Z" + str(dist) + " F" + str(speed))
  283. if currentlyAbsolute and relative:
  284. # set to absolute positioning
  285. self.sendGCode("G90")
  286. if (not currentlyAbsolute) and (not relative):
  287. # set to relative positioning
  288. self.sendGCode("G91")
  289. def callPauseResume(self):
  290. if self.stateSafetyCheck("pause/resume"):
  291. return
  292. if self.isPaused():
  293. self.sendPostRequest("printer/print/pause", "")
  294. else:
  295. self.sendPostRequest("printer/print/resume", "")
  296. def callJobCancel(self):
  297. if self.stateSafetyCheck("cancel"):
  298. return
  299. self.sendPostRequest("printer/print/cancel")
  300. def statusDialog(self):
  301. progress = self.getProgress()
  302. s = self.getName() + "\n"
  303. warning = False
  304. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  305. s += "%.1f%% Completion\n" % progress["completion"]
  306. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  307. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  308. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  309. s += "No job is currently running"
  310. else:
  311. s += "Could not read printer status!"
  312. warning = True
  313. t = self.getTemperatureString()
  314. if len(t) > 0:
  315. s += "\n" + t
  316. self.parent.showDialog("OctoTray Status", s, None, False, warning)
  317. #################
  318. # File Handling #
  319. #################
  320. def getRecentFiles(self, count):
  321. files = []
  322. r = self.sendGetRequest("server/files/directory")
  323. if (r == "timeout") or (r == "error"):
  324. return files
  325. try:
  326. rd = json.loads(r)
  327. if "result" in rd:
  328. if "files" in rd["result"]:
  329. for f in rd["result"]["files"]:
  330. if "filename" in f and "modified" in f:
  331. tmp = (f["filename"], f["modified"])
  332. files.append(tmp)
  333. except json.JSONDecodeError:
  334. pass
  335. files.sort(reverse = True, key = lambda x: x[1])
  336. files = files[:count]
  337. return [ ( i[0], i[0] ) for i in files ]
  338. def printFile(self, path):
  339. self.sendPostRequest("printer/print/start?filename=" + path, "")
  340. ###############
  341. # Temperature #
  342. ###############
  343. # only used internally
  344. def setTemperature(self, cmd, temp):
  345. cmd_str = cmd + " " + str(int(temp))
  346. self.sendGCode(cmd_str)
  347. def printerHeatTool(self, temp):
  348. self.setTemperature("M104", temp)
  349. def printerHeatBed(self, temp):
  350. self.setTemperature("M140", temp)
  351. def printerCooldown(self):
  352. if self.stateSafetyCheck("cool it down"):
  353. return
  354. self.printerHeatTool(0)
  355. self.printerHeatBed(0)
  356. ##########
  357. # Webcam #
  358. ##########
  359. def getWebcamURL(self):
  360. url = ""
  361. r = self.sendGetRequest("server/webcams/list")
  362. if (r == "timeout") or (r == "error"):
  363. return url
  364. try:
  365. rd = json.loads(r)
  366. if "result" in rd:
  367. if "webcams" in rd["result"]:
  368. if len(rd["result"]["webcams"]) > self.webcamIndex:
  369. w = rd["result"]["webcams"][self.webcamIndex]
  370. if "snapshot_url" in w:
  371. url = w["snapshot_url"]
  372. except json.JSONDecodeError:
  373. pass
  374. # make relative paths absolute
  375. if url.startswith("/"):
  376. url = "http://" + self.host + url
  377. return url