From 5402a0b16ff8f185548042b353cc185785e9fc02 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sat, 2 May 2020 18:56:53 +0200 Subject: [PATCH] [modules/octoprint] added new octoprint module * shows printer bed + tools temperature in status bar * opens popup on left click to show temperature details and webcam stream (if enabled) --- modules/contrib/octoprint.py | 224 ++++++++++++++++++++++++++++++++ themes/icons/awesome-fonts.json | 5 +- 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 modules/contrib/octoprint.py diff --git a/modules/contrib/octoprint.py b/modules/contrib/octoprint.py new file mode 100644 index 0000000..db0fd4a --- /dev/null +++ b/modules/contrib/octoprint.py @@ -0,0 +1,224 @@ +# pylint: disable=C0111,R0903 + +"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar. + + Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled). + +Parameters: + * octoprint.address : Octoprint address (e.q: http://192.168.1.3) + * octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface) + * octoprint.webcam : Set to True if a webcam is connected (default: False) +""" + + +import urllib +import logging +import threading +import queue + +import tkinter as tk +from io import BytesIO +from PIL import Image, ImageTk + +import requests +import simplejson + +import core.module +import core.widget +import core.input + +def get_frame(url): + img_bytes = b"" + stream = urllib.request.urlopen(url) + while True: + img_bytes += stream.read(1024) + a = img_bytes.find(b'\xff\xd8') + b = img_bytes.find(b'\xff\xd9') + if a != -1 and b != -1: + jpg = img_bytes[a:b+2] + img_bytes = img_bytes[b+2:] + img = Image.open(BytesIO(jpg)) + return img + return None + +class WebcamImagesWorker(threading.Thread): + def __init__(self, url, queue): + threading.Thread.__init__(self) + + self.__url = url + self.__queue = queue + self.__running = True + def run(self): + while self.__running: + img = get_frame(self.__url) + self.__queue.put(img) + + def stop(self): + self.__running = False + +class Module(core.module.Module): + @core.decorators.every(minutes=1) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.octoprint_status)) + + self.__octoprint_state = "Unknown" + self.__octoprint_address = self.parameter("address", "") + self.__octoprint_api_token = self.parameter("apitoken", "") + self.__octoprint_webcam = self.parameter("webcam", False) + + self.__webcam_images_worker = None + self.__webcam_image_url = self.__octoprint_address + "/webcam/?action=stream" + self.__webcam_images_queue = None + + self.__printer_bed_temperature = "-" + self.__tool1_temperature = "-" + + self.update_status() + + core.input.register(self, button=core.input.LEFT_MOUSE, + cmd=self.__show_popup) + + def octoprint_status(self, widget): + if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown": + return self.__octoprint_state + return self.__octoprint_state + " | B: " + str(self.__printer_bed_temperature) + "°C" + " | T1: " + str(self.__tool1_temperature) + "°C" + + def __get(self, endpoint): + url = self.__octoprint_address + "/api/" + endpoint + headers = {"X-Api-Key": self.__octoprint_api_token} + resp = requests.get(url, headers=headers) + + try: + return resp.json(), resp.status_code + except simplejson.errors.JSONDecodeError: + return None, resp.status_code + + def __get_printer_bed_temperature(self): + printer_info, status_code = self.__get("printer") + if status_code == 200: + return printer_info["temperature"]["bed"]["actual"], printer_info["temperature"]["bed"]["target"] + return None, None + + def __get_octoprint_state(self): + job_info, status_code = self.__get("job") + return job_info["state"] if status_code == 200 else "Unknown" + + def __get_tool_temperatures(self): + tool_temperatures = [] + + printer_info, status_code = self.__get("printer") + if status_code == 200: + temperatures = printer_info["temperature"] + + tool_id = 0 + while True: + try: + tool = temperatures["tool" + str(tool_id)] + tool_temperatures.append((tool["actual"], tool["target"])) + except KeyError: + break + tool_id += 1 + return tool_temperatures + + def update_status(self): + try: + self.__octoprint_state = self.__get_octoprint_state() + + actual_temp, _ = self.__get_printer_bed_temperature() + if actual_temp is None: + actual_temp = "-" + self.__printer_bed_temperature = str(actual_temp) + + tool_temps = self.__get_tool_temperatures() + if len(tool_temps) > 0: + self.__tool1_temperature = tool_temps[0][0] + else: + self.__tool1_temperature = "-" + except Exception as e: + logging.exception("Couldn't get data") + + def __refresh_image(self, root, webcam_image, webcam_image_container): + try: + img = self.__webcam_images_queue.get() + webcam_image = ImageTk.PhotoImage(img) + webcam_image_container.config(image=webcam_image) + except queue.Empty as e: + pass + except Exception as e: + logging.exception("Couldn't refresh image") + + root.after(5, self.__refresh_image, root, webcam_image, webcam_image_container) + + def __refresh_temperatures(self, root, printer_bed_temperature_label, tools_temperature_label): + actual_bed_temp, target_bed_temp = self.__get_printer_bed_temperature() + if actual_bed_temp is None: + actual_bed_temp = "-" + if target_bed_temp is None: + target_bed_temp = "-" + + bed_temp = "Bed: " + str(actual_bed_temp) + "/" + str(target_bed_temp) + " °C" + printer_bed_temperature_label.config(text=bed_temp) + + + tool_temperatures = self.__get_tool_temperatures() + tools_temp = "Tools: " + + if len(tool_temperatures) == 0: + tools_temp += "-/- °C" + else: + for i, tool_temperature in enumerate(tool_temperatures): + tools_temp += str(tool_temperature[0]) + "/" + str(tool_temperature[1]) + "°C" + if i != len(tool_temperatures)-1: + tools_temp += "\t" + tools_temperature_label.config(text=tools_temp) + + root.after(500, self.__refresh_temperatures, root, printer_bed_temperature_label, tools_temperature_label) + + def __show_popup(self, widget): + root = tk.Tk() + root.attributes('-type', 'dialog') + root.title("Octoprint") + frame = tk.Frame(root) + if self.__octoprint_webcam: + + #load first image synchronous before popup is shown, otherwise tkinter isn't able to layout popup properly + img = get_frame(self.__webcam_image_url) + webcam_image = ImageTk.PhotoImage(img) + webcam_image_container = tk.Button(frame, image=webcam_image) + webcam_image_container.pack() + + self.__webcam_images_queue = queue.Queue() + + self.__webcam_images_worker = WebcamImagesWorker(self.__webcam_image_url, self.__webcam_images_queue) + self.__webcam_images_worker.start() + else: + logging.debug("Not using webcam, as webcam is disabled. Enable with --webcam.") + frame.pack() + + temperatures_label = tk.Label(frame, text="Temperatures", font=('', 25)) + temperatures_label.pack() + + printer_bed_temperature_label = tk.Label(frame, text="Bed: -/- °C", font=('', 15)) + printer_bed_temperature_label.pack() + + tools_temperature_label = tk.Label(frame, text="Tools: -/- °C", font=('', 15)) + tools_temperature_label.pack() + + root.after(10, self.__refresh_image, root, webcam_image, webcam_image_container) + root.after(500, self.__refresh_temperatures, root, printer_bed_temperature_label, tools_temperature_label) + root.bind("", self.__on_close_popup) + + root.eval('tk::PlaceWindow . center') + root.mainloop() + + def __on_close_popup(self, event): + self.__webcam_images_queue = None + self.__webcam_images_worker.stop() + + def update(self): + self.update_status() + + def state(self, widget): + return [] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index a7fc670..240569b 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -266,5 +266,8 @@ "work": { "prefix": "" }, "break": { "prefix": "" } }, - "hddtemp": { "prefix": "" } + "hddtemp": { "prefix": "" }, + "octoprint": { + "prefix": " " + } }