Skip to content

Commit

Permalink
power: initial support for Klipper Devices
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Lamas <[email protected]>
  • Loading branch information
pedrolamas authored Jan 29, 2022
1 parent 7c8c0e7 commit 287982a
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 13 deletions.
60 changes: 47 additions & 13 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions moonraker/components/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, config: ConfigHelper) -> None:
logging.info(f"Power component loading devices: {prefix_sections}")
dev_types = {
"gpio": GpioDevice,
"klipper_device": KlipperDevice,
"tplink_smartplug": TPLinkSmartPlug,
"tasmota": Tasmota,
"shelly": Shelly,
Expand Down Expand Up @@ -514,6 +515,105 @@ def close(self) -> None:
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
Expand Down

0 comments on commit 287982a

Please sign in to comment.