diff --git a/docs/configuration.md b/docs/configuration.md index 6f95e82..3e2b09e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -332,8 +332,9 @@ The following configuration options are available for all power device types: [power device_name] type: -# The type of device. Can be either gpio, rf, tplink_smartplug, tasmota -# shelly, homeseer, homeassistant, loxonev1, or mqtt. +# The type of device. Can be either gpio, klipper_device, rf, +# tplink_smartplug, tasmota, shelly, homeseer, homeassistant, loxonev1, +# or mqtt. # This parameter must be provided. off_when_shutdown: False # If set to True the device will be powered off when Klipper enters @@ -407,16 +408,12 @@ initial_state: off timer: # A time (in seconds) after which the device will power off after being. # switched on. This effectively turns the device into a momentary switch. -# This option is available for gpio, tplink_smartplug, shelly, and tasmota -# devices. The timer may be a floating point value for gpio types, it should -# be an integer for all other types. The default is no timer is set. +# This option is available for gpio, klipper_device, tplink_smartplug, +# shelly, and tasmota devices. The timer may be a floating point value +# for gpio types, it should be an integer for all other types. The +# default is no timer is set. ``` -!!! Note - Moonraker can only be used to toggle host device GPIOs (ie: GPIOs on your - PC or SBC). Moonraker cannot control GPIOs on an MCU, Klipper should be - used for this purpose. - Examples: ```ini @@ -443,6 +440,42 @@ pin: gpiochip0/gpio17 initial_state: on ``` +#### Klipper Device Configuration + +The following options are available for `klipper_device` device types: + +```ini +# moonraker.conf + +object_name: output_pin my_pin +# The Klipper object_name (as defined in your Klipper config). Valid examples: +# output_pin my_pin +# This parameter must be provided for "klipper_device" type devices. +# Currently, only `output_pin` Klipper devices are supported. +timer: +# A time (in seconds) after which the device will power off after being. +# switched on. This effectively turns the device into a momentary switch. +# This option is available for gpio, klipper_device, tplink_smartplug, +# shelly, and tasmota devices. The timer may be a floating point value +# for gpio types, it should be an integer for all other types. The +# default is no timer is set. +``` + +!!! Note + These devices cannot be used to toggle Klipper's power supply as they + require Klipper to actually be running. + +Examples: + +```ini +# moonraker.conf + +# Control a relay providing power to the printer +[power my_pin] +type: klipper_device +object_name: output_pin my_pin +``` + #### RF Device Configuration The following options are available for gpio controlled `rf` device types: @@ -466,9 +499,10 @@ initial_state: off timer: # A time (in seconds) after which the device will power off after being. # switched on. This effectively turns the device into a momentary switch. -# This option is available for gpio, tplink_smartplug, shelly, and tasmota -# devices. The timer may be a floating point value for gpio types, it should -# be an integer for all other types. The default is no timer is set. +# This option is available for gpio, klipper_device, tplink_smartplug, +# shelly, and tasmota devices. The timer may be a floating point value +# for gpio types, it should be an integer for all other types. The +# default is no timer is set. on_code: off_code: # Valid binary codes that are sent via the RF transmitter. diff --git a/moonraker/components/power.py b/moonraker/components/power.py index cb5c0b7..cd0cbb5 100644 --- a/moonraker/components/power.py +++ b/moonraker/components/power.py @@ -44,6 +44,7 @@ class PrinterPower: logging.info(f"Power component loading devices: {prefix_sections}") dev_types = { "gpio": GpioDevice, + "klipper_device": KlipperDevice, "tplink_smartplug": TPLinkSmartPlug, "tasmota": Tasmota, "shelly": Shelly, @@ -514,6 +515,105 @@ class GpioDevice(PowerDevice): self.timer_handle.cancel() self.timer_handle = None +class KlipperDevice(PowerDevice): + def __init__(self, + config: ConfigHelper, + initial_val: Optional[int] = None + ) -> None: + if config.getboolean('off_when_shutdown', None) is not None: + raise config.error( + "Option 'off_when_shutdown' in section " + f"[{config.get_name()}] is unsupported for 'klipper_device'") + if config.getboolean('klipper_restart', None) is not None: + raise config.error( + "Option 'klipper_restart' in section " + f"[{config.get_name()}] is unsupported for 'klipper_device'") + super().__init__(config) + self.off_when_shutdown = False + self.klipper_restart = False + self.timer: Optional[float] = config.getfloat('timer', None) + if self.timer is not None and self.timer < 0.000001: + raise config.error( + f"Option 'timer' in section [{config.get_name()}] must " + "be above 0.0") + self.timer_handle: Optional[asyncio.TimerHandle] = None + self.object_name = config.get('object_name', '') + if not self.object_name.startswith("output_pin "): + raise config.error( + "Currently only Klipper 'output_pin' objects supported for " + f"'object_name' in section [{config.get_name()}]") + + self.server.register_event_handler( + "server:status_update", self._status_update) + self.server.register_event_handler( + "server:klippy_ready", self._handle_ready) + self.server.register_event_handler( + "server:klippy_disconnect", self._handle_disconnect) + + def _status_update(self, data: Dict[str, Any]) -> None: + self._set_state_from_data(data) + + async def _handle_ready(self) -> None: + kapis: APIComp = self.server.lookup_component('klippy_apis') + sub: Dict[str, Optional[List[str]]] = {self.object_name: None} + try: + data = await kapis.subscribe_objects(sub) + self._set_state_from_data(data) + except self.server.error as e: + logging.info(f"Error subscribing to {self.object_name}") + + async def _handle_disconnect(self) -> None: + self._set_state("init") + + def process_klippy_shutdown(self) -> None: + self._set_state("init") + + def refresh_status(self) -> None: + pass + + async def set_power(self, state) -> None: + if self.timer_handle is not None: + self.timer_handle.cancel() + self.timer_handle = None + try: + kapis: APIComp = self.server.lookup_component('klippy_apis') + object_name = self.object_name[len("output_pin "):] + object_name_value = "1" if state == "on" else "0" + await kapis.run_gcode( + f"SET_PIN PIN={object_name} VALUE={object_name_value}") + except Exception: + self.state = "error" + msg = f"Error Toggling Device Power: {self.name}" + logging.exception(msg) + raise self.server.error(msg) from None + self.state = state + self._check_timer() + + def _set_state_from_data(self, data) -> None: + if self.object_name not in data: + return + is_on = data.get(self.object_name, {}).get('value', 0.0) > 0.0 + state = "on" if is_on else "off" + self._set_state(state) + + def _set_state(self, state) -> None: + last_state = self.state + self.state = state + if last_state != state: + self.notify_power_changed() + + def _check_timer(self): + if self.state == "on" and self.timer is not None: + event_loop = self.server.get_event_loop() + power: PrinterPower = self.server.lookup_component("power") + self.timer_handle = event_loop.delay_callback( + self.timer, power.set_device_power, self.name, "off") + + def close(self) -> None: + if self.timer_handle is not None: + self.timer_handle.cancel() + self.timer_handle = None + class RFDevice(GpioDevice): # Protocol definition