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.

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