Skip to content

Commit

Permalink
Add togglePSU API Command (#33)
Browse files Browse the repository at this point in the history
Add getPSUState API Command (#34)
Set initial state of the switching GPIO accordingly (#40)
Add Pseudo On/Off G-Code Switching (#26)
Added M106 to autoOnTriggerGCodeCommands defaults (#45)
Move static power off dialog from jinja2 template to js (#46)
Change power off dialog message
Add invert and pull-up/down options for Sensing (#39)
Add support for sensing via system commands
  • Loading branch information
kantlivelong committed Sep 17, 2017
1 parent e91e5f2 commit 5a404bb
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 85 deletions.
26 changes: 3 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ This OctoPrint plugin controls an ATX/AUX power supply to help reduce power cons

Power supply can be automatically switched on when user specified commands are sent to the printer and/or switched off when idle.

Supports Commands (G-Code or System) or GPIO to switch power supply on/off.

**Requires a Raspberry Pi**

![PSUControl](psucontrol_navbar_settings.png?raw=true)
Expand All @@ -15,27 +13,9 @@ Supports Commands (G-Code or System) or GPIO to switch power supply on/off.
Install the plugin using Plugin Manager from Settings


## GPIO Setup

**NOTE: GPIO pins should be specified as phsyical number and not BCM number.**

###### Sense GPIO Pin
     This option is used to determine the on/off state of the power supply instead of assuming based on the last action.

     The specified GPIO pin should receive a 3.3v signal from the power supply when it is on and 0v when off. If your power supply does not provide 3.3v then consider using a [Voltage Divider](https://en.wikipedia.org/wiki/Voltage_divider).


###### On/Off GPIO Pin
     This option is only required if using GPIO instead of Commands(G-Code) to switch the power supply on/off.

     The specified GPIO pin will send a 3.3v signal when turning the power supply on and 0v when off. ATX power supplies can be switched on by grounding the PS_ON pin using a [NPN Transistor](https://en.wikipedia.org/wiki/Bipolar_junction_transistor). For "always on" power supplies use a relay to switch AC mains.
## Settings
See the [Wiki](https://github.com/kantlivelong/OctoPrint-PSUControl/wiki/Settings)


## Troubleshooting
- **The power indicator is out of sync with the power supply state.**

There are a handful of factors that play into this when not using *Sensing*. Use *Sensing* for the best experience.

- **"No access to /dev/mem. Try running as root!"**

See [Access GPIO pins without root. No access to /dev/mem. Try running as root!](https://raspberrypi.stackexchange.com/questions/40105/access-gpio-pins-without-root-no-access-to-dev-mem-try-running-as-root)
See the [Wiki](https://github.com/kantlivelong/OctoPrint-PSUControl/wiki/Troubleshooting)
162 changes: 139 additions & 23 deletions octoprint_psucontrol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from octoprint.util import RepeatedTimer
import RPi.GPIO as GPIO
import time
import subprocess
import threading
import os
from flask import make_response
from flask import make_response, jsonify

class PSUControl(octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
Expand All @@ -33,6 +34,9 @@ def __init__(self):
self.offGCodeCommand = ''
self.onSysCommand = ''
self.offSysCommand = ''
self.enablePseudoOnOff = False
self.pseudoOnGCodeCommand = ''
self.pseudoOffGCodeCommand = ''
self.postOnDelay = 0.0
self.autoOn = False
self.autoOnTriggerGCodeCommands = ''
Expand All @@ -43,9 +47,12 @@ def __init__(self):
self.idleIgnoreCommands = ''
self._idleIgnoreCommandsArray = []
self.idleTimeoutWaitTemp = 0
self.enableSensing = False
self.disconnectOnPowerOff = False
self.sensingMethod = ''
self.senseGPIOPin = 0
self.invertsenseGPIOPin = False
self.senseGPIOPinPUD = ''
self.senseSystemCommand = ''
self.isPSUOn = False
self._noSensing_isPSUOn = False
self._checkPSUTimer = None
Expand Down Expand Up @@ -79,18 +86,40 @@ def on_settings_initialized(self):
self.offSysCommand = self._settings.get(["offSysCommand"])
self._logger.debug("offSysCommand: %s" % self.offSysCommand)

self.enablePseudoOnOff = self._settings.get_boolean(["enablePseudoOnOff"])
self._logger.debug("enablePseudoOnOff: %s" % self.enablePseudoOnOff)

if self.enablePseudoOnOff and self.switchingMethod == 'GCODE':
self._logger.warning("Pseudo On/Off cannot be used in conjunction with GCODE switching.")
self.enablePseudoOnOff = False

self.pseudoOnGCodeCommand = self._settings.get(["pseudoOnGCodeCommand"])
self._logger.debug("pseudoOnGCodeCommand: %s" % self.pseudoOnGCodeCommand)

self.pseudoOffGCodeCommand = self._settings.get(["pseudoOffGCodeCommand"])
self._logger.debug("pseudoOffGCodeCommand: %s" % self.pseudoOffGCodeCommand)

self.postOnDelay = self._settings.get_float(["postOnDelay"])
self._logger.debug("postOnDelay: %s" % self.postOnDelay)

self.enableSensing = self._settings.get_boolean(["enableSensing"])
self._logger.debug("enableSensing: %s" % self.enableSensing)

self.disconnectOnPowerOff = self._settings.get_boolean(["disconnectOnPowerOff"])
self._logger.debug("disconnectOnPowerOff: %s" % self.disconnectOnPowerOff)

self.sensingMethod = self._settings.get(["sensingMethod"])
self._logger.debug("sensingMethod: %s" % self.sensingMethod)

self.senseGPIOPin = self._settings.get_int(["senseGPIOPin"])
self._logger.debug("senseGPIOPin: %s" % self.senseGPIOPin)

self.invertsenseGPIOPin = self._settings.get_boolean(["invertsenseGPIOPin"])
self._logger.debug("invertsenseGPIOPin: %s" % self.invertsenseGPIOPin)

self.senseGPIOPinPUD = self._settings.get(["senseGPIOPinPUD"])
self._logger.debug("senseGPIOPinPUD: %s" % self.senseGPIOPinPUD)

self.senseSystemCommand = self._settings.get(["senseSystemCommand"])
self._logger.debug("senseSystemCommand: %s" % self.senseSystemCommand)

self.autoOn = self._settings.get_boolean(["autoOn"])
self._logger.debug("autoOn: %s" % self.autoOn)

Expand Down Expand Up @@ -174,11 +203,19 @@ def _configure_gpio(self):
else:
return

if self.enableSensing:
self._logger.info("Using sensing to determine PSU on/off state.")
if self.sensingMethod == 'GPIO':
self._logger.info("Using GPIO sensing to determine PSU on/off state.")
self._logger.info("Configuring GPIO for pin %s" % self.senseGPIOPin)

if self.senseGPIOPinPUD == 'PULL_UP':
pudsenseGPIOPin = GPIO.PUD_UP
elif self.senseGPIOPinPUD == 'PULL_DOWN':
pudsenseGPIOPin = GPIO.PUD_DOWN
else:
pudsenseGPIOPin = GPIO.PUD_OFF

try:
GPIO.setup(self._gpio_get_pin(self.senseGPIOPin), GPIO.IN)
GPIO.setup(self._gpio_get_pin(self.senseGPIOPin), GPIO.IN, pull_up_down=pudsenseGPIOPin)
self._configuredGPIOPins.append(self.senseGPIOPin)
except (RuntimeError, ValueError) as e:
self._logger.error(e)
Expand All @@ -191,16 +228,21 @@ def _configure_gpio(self):
self._logger.info("Using GPIO for On/Off")
self._logger.info("Configuring GPIO for pin %s" % self.onoffGPIOPin)
try:
GPIO.setup(self._gpio_get_pin(self.onoffGPIOPin), GPIO.OUT)
if not self.invertonoffGPIOPin:
initial_pin_output=GPIO.LOW
else:
initial_pin_output=GPIO.HIGH
GPIO.setup(self._gpio_get_pin(self.onoffGPIOPin), GPIO.OUT, initial=initial_pin_output)
self._configuredGPIOPins.append(self.onoffGPIOPin)
except (RuntimeError, ValueError) as e:
self._logger.error(e)

def check_psu_state(self):
old_isPSUOn = self.isPSUOn

if self.enableSensing:
if self.sensingMethod == 'GPIO':
self._logger.debug("Polling PSU state...")

r = 0
try:
r = GPIO.input(self._gpio_get_pin(self.senseGPIOPin))
Expand All @@ -209,9 +251,28 @@ def check_psu_state(self):
self._logger.debug("Result: %s" % r)

if r==1:
self.isPSUOn = True
new_isPSUOn = True
elif r==0:
self.isPSUOn = False
new_isPSUOn = False

if self.invertsenseGPIOPin:
new_isPSUOn = not new_isPSUOn

self.isPSUOn = new_isPSUOn
elif self.sensingMethod == 'SYSTEM':
new_isPSUOn = False

p = subprocess.Popen(self.senseSystemCommand, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
output = p.communicate()[0]
r = p.returncode
self._logger.debug("System command returned: %s" % r)

if r==0:
new_isPSUOn = True
elif r==1:
new_isPSUOn = False

self.isPSUOn = new_isPSUOn
else:
self.isPSUOn = self._noSensing_isPSUOn

Expand Down Expand Up @@ -294,7 +355,19 @@ def _wait_for_heaters(self):
time.sleep(5)

def hook_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs):
skipQueuing = False

if gcode:
if self.enablePseudoOnOff:
if gcode == self.pseudoOnGCodeCommand:
self.turn_psu_on()
comm_instance._log("PSUControl: ok")
skipQueuing = True
elif gcode == self.pseudoOffGCodeCommand:
self.turn_psu_off()
comm_instance._log("PSUControl: ok")
skipQueuing = True

if (not self.isPSUOn and self.autoOn and (gcode in self._autoOnTriggerGCodeCommandsArray)):
self._logger.info("Auto-On - Turning PSU On (Triggered by %s)" % gcode)
self.turn_psu_on()
Expand All @@ -304,6 +377,9 @@ def hook_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args,
self._waitForHeaters = False
self._start_idle_timer()

if skipQueuing:
return (None,)

def turn_psu_on(self):
if self.switchingMethod == 'GCODE' or self.switchingMethod == 'GPIO' or self.switchingMethod == 'SYSTEM':
self._logger.info("Switching PSU On")
Expand All @@ -326,7 +402,7 @@ def turn_psu_on(self):
except (RuntimeError, ValueError) as e:
self._logger.error(e)

if not self.enableSensing:
if self.sensingMethod not in ('GPIO','SYSTEM'):
self._noSensing_isPSUOn = True

time.sleep(0.1 + self.postOnDelay)
Expand Down Expand Up @@ -357,7 +433,7 @@ def turn_psu_off(self):
if self.disconnectOnPowerOff:
self._printer.disconnect()

if not self.enableSensing:
if self.sensingMethod not in ('GPIO','SYSTEM'):
self._noSensing_isPSUOn = False

time.sleep(0.1)
Expand All @@ -366,7 +442,9 @@ def turn_psu_off(self):
def get_api_commands(self):
return dict(
turnPSUOn=[],
turnPSUOff=[]
turnPSUOff=[],
togglePSU=[],
getPSUState=[]
)

def on_api_command(self, command, data):
Expand All @@ -377,6 +455,13 @@ def on_api_command(self, command, data):
self.turn_psu_on()
elif command == 'turnPSUOff':
self.turn_psu_off()
elif command == 'togglePSU':
if self.isPSUOn:
self.turn_psu_off()
else:
self.turn_psu_on()
elif command == 'getPSUState':
return jsonify(isPSUOn=self.isPSUOn)

def get_settings_defaults(self):
return dict(
Expand All @@ -388,12 +473,18 @@ def get_settings_defaults(self):
offGCodeCommand = 'M81',
onSysCommand = '',
offSysCommand = '',
enablePseudoOnOff = False,
pseudoOnGCodeCommand = 'M80',
pseudoOffGCodeCommand = 'M81',
postOnDelay = 0.0,
enableSensing = False,
disconnectOnPowerOff = False,
sensingMethod = '',
senseGPIOPin = 0,
invertsenseGPIOPin = False,
senseGPIOPinPUD = '',
senseSystemCommand = '',
autoOn = False,
autoOnTriggerGCodeCommands = "G0,G1,G2,G3,G10,G11,G28,G29,G32,M104,M109,M140,M190",
autoOnTriggerGCodeCommands = "G0,G1,G2,G3,G10,G11,G28,G29,G32,M104,M106,M109,M140,M190",
enablePowerOffWarningDialog = True,
powerOffWhenIdle = False,
idleTimeout = 30,
Expand All @@ -404,10 +495,12 @@ def get_settings_defaults(self):
def on_settings_save(self, data):
old_GPIOMode = self.GPIOMode
old_onoffGPIOPin = self.onoffGPIOPin
old_enableSensing = self.enableSensing
old_sensingMethod = self.sensingMethod
old_senseGPIOPin = self.senseGPIOPin
old_invertsenseGPIOPin = self.invertsenseGPIOPin
old_senseGPIOPinPUD = self.senseGPIOPinPUD
old_switchingMethod = self.switchingMethod

octoprint.plugin.SettingsPlugin.on_settings_save(self, data)

self.GPIOMode = self._settings.get(["GPIOMode"])
Expand All @@ -418,10 +511,16 @@ def on_settings_save(self, data):
self.offGCodeCommand = self._settings.get(["offGCodeCommand"])
self.onSysCommand = self._settings.get(["onSysCommand"])
self.offSysCommand = self._settings.get(["offSysCommand"])
self.enablePseudoOnOff = self._settings.get_boolean(["enablePseudoOnOff"])
self.pseudoOnGCodeCommand = self._settings.get(["pseudoOnGCodeCommand"])
self.pseudoOffGCodeCommand = self._settings.get(["pseudoOffGCodeCommand"])
self.postOnDelay = self._settings.get_float(["postOnDelay"])
self.enableSensing = self._settings.get_boolean(["enableSensing"])
self.disconnectOnPowerOff = self._settings.get_boolean(["disconnectOnPowerOff"])
self.sensingMethod = self._settings.get(["sensingMethod"])
self.senseGPIOPin = self._settings.get_int(["senseGPIOPin"])
self.invertsenseGPIOPin = self._settings.get_boolean(["invertsenseGPIOPin"])
self.senseGPIOPinPUD = self._settings.get(["senseGPIOPinPUD"])
self.senseSystemCommand = self._settings.get(["senseSystemCommand"])
self.autoOn = self._settings.get_boolean(["autoOn"])
self.autoOnTriggerGCodeCommands = self._settings.get(["autoOnTriggerGCodeCommands"])
self._autoOnTriggerGCodeCommandsArray = self.autoOnTriggerGCodeCommands.split(',')
Expand All @@ -431,18 +530,27 @@ def on_settings_save(self, data):
self.enablePowerOffWarningDialog = self._settings.get_boolean(["enablePowerOffWarningDialog"])
self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',')
self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"])


#GCode switching and PseudoOnOff are not compatible.
if self.switchingMethod == 'GCODE' and self.enablePseudoOnOff:
self.enablePseudoOnOff = False
self._settings.set_boolean(["enablePseudoOnOff"], self.enablePseudoOnOff)
self._settings.save()


if (old_GPIOMode != self.GPIOMode or
old_onoffGPIOPin != self.onoffGPIOPin or
old_senseGPIOPin != self.senseGPIOPin or
old_enableSensing != self.enableSensing or
old_sensingMethod != self.sensingMethod or
old_invertsenseGPIOPin != self.invertsenseGPIOPin or
old_senseGPIOPinPUD != self.senseGPIOPinPUD or
old_switchingMethod != self.switchingMethod):
self._configure_gpio()

self._start_idle_timer()

def get_settings_version(self):
return 2
return 3

def on_settings_migrate(self, target, current=None):
if current is None or current < 2:
Expand Down Expand Up @@ -470,6 +578,14 @@ def on_settings_migrate(self, target, current=None):
self._settings.set(["autoOnTriggerGCodeCommands"], cur_autoOnCommands)
self._settings.remove(["autoOnCommands"])

if current < 3:
# v3 adds support for multiple sensing methods
cur_enableSensing = self._settings.get_boolean(["enableSensing"])
if cur_enableSensing is not None and cur_enableSensing:
self._logger.info("Migrating Setting: enableSensing=True -> sensingMethod=GPIO")
self._settings.set(["sensingMethod"], "GPIO")
self._settings.remove(["enableSensing"])

def get_template_configs(self):
return [
dict(type="settings", custom_bindings=False)
Expand Down
Loading

0 comments on commit 5a404bb

Please sign in to comment.