diff --git a/docs/changelog.md b/docs/changelog.md index 5926c82c..ec5d300f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,7 @@ #### 2020 11 28 * Add option for enable in menu for configuration. This can hide certain options +* Add Power panel to control power devices via moonraker #### 2020 11 18 * Changed configuration file format. diff --git a/ks_includes/KlipperScreen.conf b/ks_includes/KlipperScreen.conf index 57637ff0..f9e5b286 100644 --- a/ks_includes/KlipperScreen.conf +++ b/ks_includes/KlipperScreen.conf @@ -80,6 +80,12 @@ name: Temperature icon: heat-up panel: temperature +[menu __main actions power] +name: Power +icon: shutdown +panel: power +enable: {{ printer.power_devices.count > 0 }} + [menu __main actions disablemotors] name: Disable Motors icon: motor-off diff --git a/ks_includes/KlippyWebsocket.py b/ks_includes/KlippyWebsocket.py index 87e4845e..e0b5cf97 100644 --- a/ks_includes/KlippyWebsocket.py +++ b/ks_includes/KlippyWebsocket.py @@ -202,6 +202,24 @@ class MoonrakerApi: updates ) + def power_device_off(self, device, callback=None, *args): + logger.debug("Sending machine.device_power.off: %s" % device) + return self._ws.send_method( + "machine.device_power.off", + {device: False}, + callback, + *args + ) + + def power_device_on(self, device, callback=None, *args): + logger.debug("Sending machine.device_power.on %s" % device) + return self._ws.send_method( + "machine.device_power.on", + {device: False}, + callback, + *args + ) + def print_cancel(self, callback=None, *args): logger.debug("Sending printer.print.cancel") return self._ws.send_method( diff --git a/ks_includes/config.py b/ks_includes/config.py index 5fe72b4d..50a569f7 100644 --- a/ks_includes/config.py +++ b/ks_includes/config.py @@ -67,6 +67,9 @@ class KlipperScreenConfig: return preheat_options + def get_printer_power_name(self): + return self.config['settings'].get("printer_power_name", "printer") + def log_config(self, config): lines = [ @@ -94,7 +97,8 @@ class KlipperScreenConfig: "icon": cfg.get("icon"), "panel": cfg.get("panel", False), "method": cfg.get("method", False), - "confirm": cfg.get("confirm", False) + "confirm": cfg.get("confirm", False), + "enable": cfg.get("enable", True) } try: diff --git a/ks_includes/printer.py b/ks_includes/printer.py index bd8fe452..ce673cfc 100644 --- a/ks_includes/printer.py +++ b/ks_includes/printer.py @@ -1,5 +1,7 @@ import logging +logger = logging.getLogger("KlipperScreen.Printer") + class Printer: def __init__(self, data): @@ -12,6 +14,7 @@ class Printer: self.devices = {} self.state = data['print_stats']['state'] self.data = data + self.power_devices = {} for x in self.config.keys(): if x.startswith('extruder'): @@ -36,6 +39,17 @@ class Printer: logging.info("### Toolcount: " + str(self.toolcount) + " Heaters: " + str(self.extrudercount)) + def configure_power_devices(self, data): + self.power_devices = {} + + logger.debug("Processing power devices: %s" % data) + for x in data['devices']: + logger.debug(x) + self.power_devices[x['device']] = { + "status": "on" if x['status'] == "on" else "off" + } + logger.debug("Power devices: %s" % self.power_devices) + def process_update(self, data): keys = ['virtual_sdcard','pause_resume','idle_timeout','print_stats'] keys = ['fan','gcode_move','idle_timeout','pause_resume','print_stats','toolhead','virtual_sdcard'] @@ -61,6 +75,10 @@ class Printer: if "temperature" in d: self.set_dev_stat(x, "temperature", d["temperature"]) + def process_power_update(self, data): + if data['device'] in self.power_devices: + self.power_devices[data['device']]['status'] = data['status'] + def get_config_section_list(self): return list(self.config) @@ -72,6 +90,14 @@ class Printer: def get_data(self): return self.data + def get_power_devices(self): + return list(self.power_devices) + + def get_power_device_status(self, device): + if device not in self.power_devices: + return + return self.power_devices[device]['status'] + def get_stat(self, stat, substat = None): if substat != None: return self.data[stat][substat] diff --git a/panels/power.py b/panels/power.py new file mode 100644 index 00000000..b386e57c --- /dev/null +++ b/panels/power.py @@ -0,0 +1,122 @@ +import gi +import logging + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib, Pango + +from ks_includes.KlippyGtk import KlippyGtk +from ks_includes.KlippyGcodes import KlippyGcodes +from ks_includes.screen_panel import ScreenPanel + +logger = logging.getLogger("KlipperScreen.PowerPanel") + +def create_panel(*args): + return PowerPanel(*args) + +class PowerPanel(ScreenPanel): + def initialize(self, panel_name): + _ = self.lang.gettext + self.devices = {} + + # Create bottom bar + bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + bar.set_hexpand(True) + bar.set_vexpand(False) + bar.set_halign(Gtk.Align.END) + bar.set_margin_top(5) + bar.set_margin_bottom(5) + bar.set_margin_end(5) + + # Add a back button to the bottom bar + back = KlippyGtk.ButtonImage('back', None, None, 60, 60) + back.connect("clicked", self._screen._menu_go_back) + bar.add(back) + + # Create a scroll window for the power devices + scroll = Gtk.ScrolledWindow() + scroll.set_property("overlay-scrolling", False) + scroll.set_vexpand(True) + + # Create a grid for all devices + self.labels['devices'] = Gtk.Grid() + scroll.add(self.labels['devices']) + + # Create a box to contain all of the above + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + box.set_vexpand(True) + box.pack_start(scroll, True, True, 0) + box.pack_end(bar, False, False, 0) + + self.load_power_devices() + + self.panel = box + self._screen.add_subscription(panel_name) + + def add_device(self, device): + + frame = Gtk.Frame() + frame.set_property("shadow-type",Gtk.ShadowType.NONE) + + name = Gtk.Label() + name.set_markup("%s" % (device)) + name.set_hexpand(True) + name.set_halign(Gtk.Align.START) + name.set_line_wrap(True) + name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) + + switch = Gtk.Switch() + switch.set_hexpand(False) + switch.set_active(True if self._screen.printer.get_power_device_status(device) == "on" else False) + switch.connect("notify::active", self.on_switch, device) + switch.set_property("width-request", 150) + switch.set_property("height-request", 80) + + labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + labels.add(name) + + dev = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + dev.set_margin_top(10) + dev.set_margin_end(15) + dev.set_margin_start(15) + dev.set_margin_bottom(10) + dev.set_hexpand(True) + dev.set_vexpand(False) + + dev.add(labels) + dev.add(switch) + frame.add(dev) + + self.devices[device] = { + "row": frame, + "switch": switch + } + + devices = sorted(self.devices) + pos = devices.index(device) + + self.labels['devices'].insert_row(pos) + self.labels['devices'].attach(self.devices[device]['row'], 0, pos, 1, 1) + self.labels['devices'].show_all() + + def load_power_devices(self): + devices = self._screen.printer.get_power_devices() + for x in devices: + self.add_device(x) + + def on_switch(self, switch, gparam, device): + logger.debug("Power toggled %s" % device) + if switch.get_active(): + self._screen._ws.klippy.power_device_on(device) + else: + self._screen._ws.klippy.power_device_off(device) + + def process_update(self, action, data): + if action != "notify_power_changed": + return + + if data['device'] not in self.devices: + return + device = data['device'] + self.devices[device]['switch'].disconnect_by_func(self.on_switch) + self.devices[device]['switch'].set_active(True if data['status'] == "on" else False) + self.devices[device]['switch'].connect("notify::active", self.on_switch, device) diff --git a/panels/splash_screen.py b/panels/splash_screen.py index cd8116f1..48cb07cc 100644 --- a/panels/splash_screen.py +++ b/panels/splash_screen.py @@ -3,7 +3,7 @@ import logging import os gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Gtk, Gdk, GLib, Pango from ks_includes.KlippyGtk import KlippyGtk from ks_includes.screen_panel import ScreenPanel @@ -25,6 +25,8 @@ class SplashScreenPanel(ScreenPanel): self.labels['text'] = Gtk.Label(_("Initializing printer...")) self.labels['text'].get_style_context().add_class("text") + self.labels['text'].set_line_wrap(True) + self.labels['text'].set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) self.labels['actions'] = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) @@ -57,6 +59,7 @@ class SplashScreenPanel(ScreenPanel): _ = self.lang.gettext if "firmware_restart" not in self.labels: + self.labels['power'] = KlippyGtk.ButtonImage("reboot",_("Power On Printer"),"color3") self.labels['restart'] = KlippyGtk.ButtonImage("reboot",_("Restart"),"color1") self.labels['restart'].connect("clicked", self.restart) self.labels['firmware_restart'] = KlippyGtk.ButtonImage("restart",_("Firmware Restart"),"color2") @@ -64,11 +67,22 @@ class SplashScreenPanel(ScreenPanel): self.clear_action_bar() + devices = [i for i in self._screen.printer.get_power_devices() if i.lower().startswith('printer')] + logger.debug("Power devices: %s" % devices) + if len(devices) > 0: + logger.debug("Adding power button") + self.labels['power'].connect("clicked", self.power_on, devices[0]) + self.labels['actions'].add(self.labels['power']) + + self.labels['actions'].add(self.labels['power']) self.labels['actions'].add(self.labels['restart']) self.labels['actions'].add(self.labels['firmware_restart']) def firmware_restart(self, widget): self._screen._ws.klippy.restart_firmware() + def power_on(self, widget, device): + self._screen._ws.klippy.power_device_on(device) + def restart(self, widget): self._screen._ws.klippy.restart() diff --git a/screen.py b/screen.py index 4cf4af95..07972a7f 100644 --- a/screen.py +++ b/screen.py @@ -320,6 +320,9 @@ class KlipperScreen(Gtk.Window): #self.files.add_file() elif action == "notify_metadata_update": self.files.update_metadata(data['filename']) + elif action == "notify_power_changed": + logger.debug("Power status changed: %s", data) + self.printer.process_power_update(data) elif self.shutdown == False and action == "notify_gcode_response": if "Klipper state: Shutdown" in data: self.shutdown == True @@ -384,6 +387,7 @@ class KlipperScreen(Gtk.Window): ] info = self.apiclient.get_printer_info() data = self.apiclient.send_request("printer/objects/query?" + "&".join(status_objects)) + powerdevs = self.apiclient.send_request("machine/device_power/devices") if info == False or data == False: self.printer_initializing(_("Moonraker error")) return @@ -392,6 +396,7 @@ class KlipperScreen(Gtk.Window): # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.__init__(data) self.ws_subscribe() + self.printer.configure_power_devices(powerdevs['result']) if self.files == None: self.files = KlippyFiles(self)