def test_canceled_callback(self): timer_task = mock.MagicMock() on_cancelled_cb = mock.MagicMock() timer = ResettableTimer(10, timer_task, on_cancelled=on_cancelled_cb) timer.start() time.sleep(5) timer.cancel() time.sleep(10) self.assertEqual(0, timer_task.call_count) self.assertEqual(1, on_cancelled_cb.call_count)
class TasmotaMQTTPlugin( octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.WizardPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.tasmota_mqtt") self._tasmota_mqtt_logger = logging.getLogger( "octoprint.plugins.tasmota_mqtt.debug") self.abortTimeout = 0 self._timeout_value = None self._abort_timer = None self._countdown_active = False self._waitForHeaters = False self._waitForTimelapse = False self._timelapse_active = False self._skipIdleTimer = False self.powerOffWhenIdle = False self._idleTimer = None self._autostart_file = None self.mqtt_publish = None self.mqtt_subscribe = None self.idleTimeout = None self.idleIgnoreCommands = None self._idleIgnoreCommandsArray = None self.idleTimeoutWaitTemp = None ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict(arrRelays=[], full_topic_pattern='%topic%/%prefix%/', abortTimeout=30, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50, debug_logging=False) def get_settings_version(self): return 5 def on_settings_migrate(self, target, current=None): if current is None or current < 3: self._settings.set(['arrRelays'], self.get_settings_defaults()["arrRelays"]) if current == 2: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["automaticShutdownEnabled"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) if current <= 3: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["errorEvent"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) if current <= 4: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["event_on_upload"] = False relay["event_on_startup"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) old_powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) old_idleTimeout = self._settings.get_int(["idleTimeout"]) old_idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) old_idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) if self.powerOffWhenIdle != old_powerOffWhenIdle: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle: self._tasmota_mqtt_logger.debug( "Settings saved, Automatic Power Off Enabled, starting idle timer..." ) self._reset_idle_timer() new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._tasmota_mqtt_logger.setLevel(logging.DEBUG) else: self._tasmota_mqtt_logger.setLevel(logging.INFO) ##~~ StartupPlugin mixin def on_startup(self, host, port): # setup customized logger from octoprint.logging.handlers import CleaningTimedRotatingFileHandler tasmota_mqtt_logging_hnadler = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(postfix="debug"), when="D", backupCount=3) tasmota_mqtt_logging_hnadler.setFormatter( logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) tasmota_mqtt_logging_hnadler.setLevel(logging.DEBUG) self._tasmota_mqtt_logger.addHandler(tasmota_mqtt_logging_hnadler) self._tasmota_mqtt_logger.setLevel( logging.DEBUG if self._settings.get_boolean(["debug_logging"] ) else logging.INFO) self._tasmota_mqtt_logger.propagate = False def on_after_startup(self): helpers = self._plugin_manager.get_helpers("mqtt", "mqtt_publish", "mqtt_subscribe", "mqtt_unsubscribe") if helpers: if "mqtt_subscribe" in helpers: self.mqtt_subscribe = helpers["mqtt_subscribe"] for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( self.generate_mqtt_full_topic(relay, "stat")) self.mqtt_subscribe(self.generate_mqtt_full_topic( relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top=relay["topic"], relayN=relay["relayN"])) if "mqtt_publish" in helpers: self.mqtt_publish = helpers["mqtt_publish"] self.mqtt_publish("octoprint/plugin/tasmota", "OctoPrint-TasmotaMQTT publishing.") if any( map(lambda r: r["event_on_startup"] == True, self._settings.get(["arrRelays"]))): for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( "powering on {} due to startup.".format( relay["topic"])) self.turn_on(relay) if "mqtt_unsubscribe" in helpers: self.mqtt_unsubscribe = helpers["mqtt_unsubscribe"] else: self._plugin_manager.send_plugin_message(self._identifier, dict(noMQTT=True)) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._tasmota_mqtt_logger.debug("abortTimeout: %s" % self.abortTimeout) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._tasmota_mqtt_logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._tasmota_mqtt_logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._tasmota_mqtt_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._tasmota_mqtt_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) if self.powerOffWhenIdle: self._tasmota_mqtt_logger.debug( "Starting idle timer due to startup") self._reset_idle_timer() def _on_mqtt_subscription(self, topic, message, retained=None, qos=None, *args, **kwargs): self._tasmota_mqtt_logger.debug( "Received message for {topic}: {message}".format(**locals())) self.mqtt_publish("octoprint/plugin/tasmota", "echo: " + message.decode("utf-8")) newrelays = [] bolRelayStateChanged = False bolForceIdleTimer = False for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{top}".format( **kwargs) and relay["relayN"] == "{relayN}".format( **kwargs ) and relay["currentstate"] != message.decode("utf-8"): bolRelayStateChanged = True relay["currentstate"] = message.decode("utf-8") if relay[ "automaticShutdownEnabled"] == True and self._settings.get_boolean( ["powerOffWhenIdle" ]) and relay["currentstate"] == "ON": self._tasmota_mqtt_logger.debug( "Forcing reset of idle timer because {} was just turned on." .format(relay["topic"])) bolForceIdleTimer = True self._plugin_manager.send_plugin_message( self._identifier, dict(topic="{top}".format(**kwargs), relayN="{relayN}".format(**kwargs), currentstate=message.decode("utf-8"))) newrelays.append(relay) if bolRelayStateChanged: self._settings.set(["arrRelays"], newrelays) self._settings.save() if bolForceIdleTimer: self._reset_idle_timer() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): if event == "WHERE": try: self.mqtt_unsubscribe(self._on_mqtt_subscription) for relay in self._settings.get(["arrRelays"]): self.mqtt_subscribe(self.generate_mqtt_full_topic( relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top=relay["topic"], relayN=relay["relayN"])) except: self._plugin_manager.send_plugin_message( self._identifier, dict(noMQTT=True)) # Client Opened Event if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) return # Print Started Event if event == Events.PRINT_STARTED and self.powerOffWhenIdle == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._tasmota_mqtt_logger.debug( "Power off aborted because starting new print.") if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) # Print Error Event if event == Events.ERROR: self._tasmota_mqtt_logger.debug( "Powering off enabled plugs because there was an error.") for relay in self._settings.get(['arrRelays']): if relay.get("errorEvent", False): self.turn_off(relay) # Timeplapse Events if self.powerOffWhenIdle == True and event == Events.MOVIE_RENDERING: self._tasmota_mqtt_logger.debug( "Timelapse generation started: %s" % payload.get("movie_basename", "")) self._timelapse_active = True if self._timelapse_active and event == Events.MOVIE_DONE or event == Events.MOVIE_FAILED: self._tasmota_mqtt_logger.debug( "Timelapse generation finished: %s. Return Code: %s" % (payload.get("movie_basename", ""), payload.get("returncode", "completed"))) self._timelapse_active = False # Printer Connected Event if event == Events.CONNECTED: if self._autostart_file: self._tasmota_mqtt_logger.debug( "printer connected starting print of %s" % self._autostart_file) self._printer.select_file(self._autostart_file, False, printAfterSelect=True) self._autostart_file = None # File Uploaded Event if event == Events.UPLOAD and any( map(lambda r: r["event_on_upload"] == True, self._settings.get(["arrRelays"]))): if payload.get("print", False): # implemented in OctoPrint version 1.4.1 self._tasmota_mqtt_logger.debug( "File uploaded: %s. Turning enabled relays on." % payload.get("name", "")) self._tasmota_mqtt_logger.debug(payload) for relay in self._settings.get(['arrRelays']): self._tasmota_mqtt_logger.debug(relay) if relay[ "event_on_upload"] is True and not self._printer.is_ready( ): self._tasmota_mqtt_logger.debug( "powering on %s due to %s event." % (relay["topic"], event)) if payload.get( "path", False) and payload.get("target") == "local": self._autostart_file = payload.get("path") self.turn_on(relay) ##~~ AssetPlugin mixin def get_assets(self): return dict(js=[ "js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/fontawesome-iconpicker.js", "js/ko.iconpicker.js", "js/tasmota_mqtt.js" ], css=[ "css/font-awesome.min.css", "css/font-awesome-v4-shims.min.css", "css/fontawesome-iconpicker.css", "css/tasmota_mqtt.css" ]) ##~~ TemplatePlugin mixin def get_template_configs(self): return [ dict(type="navbar", custom_bindings=True), dict(type="settings", custom_bindings=True), dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: filteredSmartplugs().length > 0", template="tasmota_mqtt_sidebar.jinja2", template_header="tasmota_mqtt_sidebar_header.jinja2") ] ##~~ SimpleApiPlugin mixin def get_api_commands(self): return dict(turnOn=["topic", "relayN"], turnOff=["topic", "relayN"], toggleRelay=["topic", "relayN"], checkRelay=["topic", "relayN"], checkStatus=[], removeRelay=["topic", "relayN"], enableAutomaticShutdown=[], disableAutomaticShutdown=[], abortAutomaticShutdown=[], getListPlug=[]) def on_api_command(self, command, data): if not Permissions.PLUGIN_TASMOTA_MQTT_CONTROL.can(): from flask import make_response return make_response("Insufficient rights", 403) if command == 'toggleRelay' or command == 'turnOn' or command == 'turnOff': for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): if command == "turnOff" or (command == "toggleRelay" and relay["currentstate"] == "ON"): self._tasmota_mqtt_logger.debug( "turning off {topic} relay {relayN}".format( **data)) self.turn_off(relay) if command == "turnOn" or (command == "toggleRelay" and relay["currentstate"] == "OFF"): self._tasmota_mqtt_logger.debug( "turning on {topic} relay {relayN}".format(**data)) self.turn_on(relay) if command == 'checkStatus': for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( "checking status of %s relay %s" % (relay["topic"], relay["relayN"])) try: self.mqtt_publish( self.generate_mqtt_full_topic(relay, "cmnd"), "") except: self._plugin_manager.send_plugin_message( self._identifier, dict(noMQTT=True)) if command == 'checkRelay': self._tasmota_mqtt_logger.debug( "subscribing to {topic} relay {relayN}".format(**data)) for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): self.mqtt_subscribe( self.generate_mqtt_full_topic(relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top="{topic}".format(**data), relayN="{relayN}".format(**data))) self._tasmota_mqtt_logger.debug( "checking {topic} relay {relayN}".format(**data)) self.mqtt_publish( self.generate_mqtt_full_topic(relay, "cmnd"), "") if command == 'removeRelay': for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): self.mqtt_unsubscribe(self._on_mqtt_subscription, topic=self.generate_mqtt_full_topic( relay, "stat")) if command == 'enableAutomaticShutdown': self.powerOffWhenIdle = True self._tasmota_mqtt_logger.debug( "Automatic Power Off enabled, starting idle timer.") self._start_idle_timer() if command == 'disableAutomaticShutdown': self.powerOffWhenIdle = False if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._tasmota_mqtt_logger.debug( "Automatic Power Off disabled, stopping idle and abort timers." ) self._stop_idle_timer() if command == 'abortAutomaticShutdown': if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._tasmota_mqtt_logger.debug("Power off aborted.") self._tasmota_mqtt_logger.debug("Restarting idle timer.") self._reset_idle_timer() if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown": self._tasmota_mqtt_logger.debug( "Automatic power off setting changed: %s" % self.powerOffWhenIdle) self._settings.set_boolean(["powerOffWhenIdle"], self.powerOffWhenIdle) self._settings.save() if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown" or command == "abortAutomaticShutdown": self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if command == "getListPlug": return json.dumps(self._settings.get(["arrRelays"])) def turn_on(self, relay): self.mqtt_publish(self.generate_mqtt_full_topic(relay, "cmnd"), "ON") if relay["sysCmdOn"]: t = threading.Timer(int(relay["sysCmdOnDelay"]), os.system, args=[relay["sysCmdRunOn"]]) t.start() if relay["connect"] and self._printer.is_closed_or_error(): t = threading.Timer(int(relay["connectOnDelay"]), self._printer.connect) t.start() if self.powerOffWhenIdle == True and relay[ "automaticShutdownEnabled"] == True: self._tasmota_mqtt_logger.debug( "Resetting idle timer since relay %s | %s was just turned on." % (relay["topic"], relay["relayN"])) self._waitForHeaters = False self._reset_idle_timer() def turn_off(self, relay): if relay["sysCmdOff"]: t = threading.Timer(int(relay["sysCmdOffDelay"]), os.system, args=[relay["sysCmdRunOff"]]) t.start() if relay["disconnect"]: self._printer.disconnect() time.sleep(int(relay["disconnectOffDelay"])) self.mqtt_publish(self.generate_mqtt_full_topic(relay, "cmnd"), "OFF") ##~~ Gcode processing hook def gcode_turn_off(self, relay): if relay["warnPrinting"] and self._printer.is_printing(): self._tasmota_mqtt_logger.debug( "Not powering off %s | %s because printer is printing." % (relay["topic"], relay["relayN"])) else: self.turn_off(relay) def processGCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if gcode: if cmd.startswith("M8") and cmd.count(" ") >= 1: topic = cmd.split()[1] if cmd.count(" ") == 2: relayN = cmd.split()[2] else: relayN = "" for relay in self._settings.get(["arrRelays"]): if relay["topic"].upper() == topic.upper( ) and relay["relayN"] == relayN and relay["gcode"]: if cmd.startswith("M80"): t = threading.Timer(int(relay["gcodeOnDelay"]), self.turn_on, [relay]) t.start() return "M80" elif cmd.startswith("M81"): ## t = threading.Timer(int(relay["gcodeOffDelay"]),self.mqtt_publish,[relay["topic"] + "/cmnd/Power" + relay["relayN"], "OFF"]) t = threading.Timer(int(relay["gcodeOffDelay"]), self.gcode_turn_off, [relay]) t.start() return "M81" elif self.powerOffWhenIdle and not ( gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() return else: return else: return ##~~ Idle Timeout def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle and any( map(lambda r: r["currentstate"] == "ON", self._settings.get(["arrRelays"]))): self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._waitForTimelapse: return if self._printer.is_printing() or self._printer.is_paused(): return if (uptime() / 60) <= (self._settings.get_int(["idleTimeout"])): self._tasmota_mqtt_logger.debug( "Just booted so wait for time sync.") self._tasmota_mqtt_logger.debug( "uptime: {}, comparison: {}".format( (uptime() / 60), (self._settings.get_int(["idleTimeout"])))) self._reset_idle_timer() return self._tasmota_mqtt_logger.debug( "Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._tasmota_mqtt_logger.debug("Heaters below temperature.") if self._wait_for_timelapse(): self._timer_start() else: self._tasmota_mqtt_logger.debug( "Aborted power off due to activity.") ##~~ Timelapse Monitoring def _wait_for_timelapse(self): self._waitForTimelapse = True self._tasmota_mqtt_logger.debug( "Checking timelapse status before shutting off power...") while True: if not self._waitForTimelapse: return False if not self._timelapse_active: self._waitForTimelapse = False return True self._tasmota_mqtt_logger.debug( "Waiting for timelapse before shutting off power...") time.sleep(5) ##~~ Temperature Cooldown def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._tasmota_mqtt_logger.debug("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._tasmota_mqtt_logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._tasmota_mqtt_logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._tasmota_mqtt_logger.debug( "Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer def _timer_start(self): if self._abort_timer is not None: return self._tasmota_mqtt_logger.debug("Starting abort power off timer.") self._timeout_value = self.abortTimeout self._abort_timer = RepeatedTimer(1, self._timer_task) self._abort_timer.start() def _timer_task(self): if self._timeout_value is None: return self._timeout_value -= 1 self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): self._tasmota_mqtt_logger.debug( "Automatically powering off enabled plugs.") for relay in self._settings.get(['arrRelays']): if relay.get("automaticShutdownEnabled", False): self.turn_off(relay) ##~~ Utility functions def generate_mqtt_full_topic(self, relay, prefix): full_topic = re.sub(r'%topic%', relay["topic"], self._settings.get(["full_topic_pattern"])) full_topic = re.sub(r'%prefix%', prefix, full_topic) full_topic = full_topic + "POWER" + relay["relayN"] return full_topic ##~~ WizardPlugin mixin def is_wizard_required(self): helpers = self._plugin_manager.get_helpers("mqtt") if helpers: return False return True ##~~ Access Permissions Hook def get_additional_permissions(self, *args, **kwargs): return [ dict(key="CONTROL", name="Control Relays", description=gettext("Allows control of configured relays."), roles=["admin"], dangerous=True, default_groups=[ADMIN_GROUP]) ] ##~~ Softwareupdate hook def get_update_information(self): return dict(tasmota_mqtt=dict( displayName="Tasmota-MQTT", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-TasmotaMQTT", current=self._plugin_version, stable_branch=dict( name="Stable", branch="master", comittish=["master"]), prerelease_branches=[ dict( name="Release Candidate", branch="rc", comittish=["rc", "master"], ) ], # update method: pip pip= "https://github.com/jneilliii/OctoPrint-TasmotaMQTT/archive/{target_version}.zip" ))
class CooldownfanPlugin( octoprint.plugin.StartupPlugin, octoprint.plugin.ShutdownPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.AssetPlugin ): def initialize(self): self.activated = 0 self.last_cooldown_pin = -1 self.turnOffTimer = None self.fanStatus = "" self._logger.info("Running RPi.GPIO version '{0}'".format(GPIO.VERSION)) if GPIO.VERSION < "0.6": # Need at least 0.6 for edge detection raise Exception("RPi.GPIO must be greater than 0.6") GPIO.setwarnings(False) # Disable GPIO warnings def on_after_startup(self): self._logger.info("CooldownFan Plugin Starting...") self._setup_sensor() @property def pin_cooldown(self): return int(self._settings.get(["pin_cooldown"])) @property def run_time(self): return int(self._settings.get(["run_time"])) @property def normal_state(self): return int(self._settings.get(["normal_state"])) @property def fan_status(self): return self.fanStatus def get_template_configs(self): return [dict(type="settings", custom_bindings=True)] def _setup_sensor(self): self.cleanup_last_channel(self.last_cooldown_pin) self.last_cooldown_pin = self.pin_cooldown if self.cooldown_pin_enabled(): GPIO.setmode(GPIO.BCM) GPIO.setup(self.pin_cooldown, GPIO.OUT, initial=self.get_off_state()) GPIO.output(self.pin_cooldown, self.get_off_state()) def cleanup_last_channel(self, channel): if channel!=-1: try: GPIO.remove_event_detect(channel) except: pass try: GPIO.cleanup(channel) except: pass def get_settings_defaults(self): return dict( pin_cooldown = -1, # Default is no pin run_time = 600, # Default 5 minutes running time normal_state = 0 # Normal (OFF) state is LOW ) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._setup_sensor() def cooldown_pin_enabled(self): return self.pin_cooldown != -1 def on_event(self, event, payload): if event == Events.PRINT_DONE: self.startCoolingDown() elif event == Events.PRINT_STARTED: self.turnOffCoolingFan() def startCoolingDown(self): self.fanStatus = "Start cooling fan on GPIO {} for {} seconds!".format(self.pin_cooldown, self.run_time) self._logger.info(self.fanStatus) self.disableFanTimer() if self.cooldown_pin_enabled(): GPIO.output(self.pin_cooldown, self.get_on_state()) self.turnOffTimer = ResettableTimer(self.get_valid_time_seconds(), self.turnOffCoolingFan) self.turnOffTimer.start() def turnOffCoolingFan(self): self.fanStatus = "Fan turned off" self.disableFanTimer() if self.cooldown_pin_enabled(): GPIO.output(self.pin_cooldown, self.get_off_state()) def disableFanTimer(self): if self.turnOffTimer!=None: self.turnOffTimer.cancel() self.turnOffTimer=None def get_valid_time_seconds(self): t = self.run_time if t<5: return 5 else: return t def get_off_state(self): if self.normal_state==0: return GPIO.LOW return GPIO.HIGH def get_on_state(self): if self.normal_state==0: return GPIO.HIGH return GPIO.LOW ##~~ simpleApiPlugin def get_api_commands(self): return dict(fan_on=["pin","time","normal"],fan_off=["pin","time","normal"],pull_status=["rnd"]) def on_api_command(self, command, data): if command == "fan_on": try: selected_pin = int(data.get("pin")) selected_time = int(data.get("time")) selected_normal = int(data.get("normal")) self._settings.set(["pin_cooldown"],selected_pin) self._settings.set(["run_time"],selected_time) self._settings.set(["normal_state"],selected_normal) self._setup_sensor() self.startCoolingDown() return flask.jsonify(dict(status="y",fanStatus=self.fanStatus)), 200 except ValueError: return flask.jsonify(dict(status="n",fanStatus=self.fanStatus)), 200 elif command == "fan_off": try: selected_pin = int(data.get("pin")) selected_time = int(data.get("time")) selected_normal = int(data.get("normal")) self._settings.set(["pin_cooldown"],selected_pin) self._settings.set(["run_time"],selected_time) self._settings.set(["normal_state"],selected_normal) self.turnOffCoolingFan() return flask.jsonify(dict(status="y",fanStatus=self.fanStatus)), 200 except ValueError: return flask.jsonify(dict(status="n",fanStatus=self.fanStatus)), 200 elif command == "pull_status": return flask.jsonify(dict(status="y",fanStatus=self.fanStatus)), 200 ##~~ AssetPlugin mixin def get_assets(self): # Define your plugin's asset files to automatically include in the # core UI here. return dict( js=["js/cooldownfan.js"], css=["css/cooldownfan.css"], less=["less/cooldownfan.less"] ) ##~~ Softwareupdate hook def get_update_information(self): # Define the configuration for your plugin to use with the Software Update # Plugin here. See https://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html # for details. return dict( cooldownfan=dict( displayName="Cooldown Fan", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Cooldownfan", current=self._plugin_version, # update method: pip pip="https://github.com/fmalekpour/OctoPrint-Cooldownfan/archive/{target_version}.zip" ) )
class Display_panelPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.ShutdownPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.ProgressPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin): _area_offset = 3 _cancel_requested_at = 0 _cancel_timer = None _colored_strip_height = 16 # height of colored strip on top for dual color display _debounce = 0 _display_init = False _displaylayerprogress_current_height = -1.0 _displaylayerprogress_current_layer = -1 _displaylayerprogress_total_height = -1.0 _displaylayerprogress_total_layer = -1 _display_timeout_active = False _display_timeout_option = 0 # -1 - deactivated, 0 - printer disconnected, 1 - disconnected/connected but idle, 2 - always _display_timeout_time = 0 _display_timeout_timer = None _etl_format = "{hours:02d}h {minutes:02d}m {seconds:02d}s" _eta_strftime = "" _gpio_init = False _image_rotate = False _last_debounce = 0 _last_display_timeout_option = 0 # -1 - deactivated, 0 - printer disconnected, 1 - disconnected/connected but idle, 2 - always _last_display_timeout_time = 0 _last_i2c_address = "" _last_image_rotate = False _last_pin_cancel = -1 _last_pin_mode = -1 _last_pin_pause = -1 _last_pin_play = -1 _last_printer_state = 0 # 0 - disconnected, 1 - connected but idle, 2 - printing _pin_cancel = -1 _pin_mode = -1 _pin_pause = -1 _pin_play = -1 _printer_state = 0 # 0 - disconnected, 1 - connected but idle, 2 - printing _progress_on_top = False _screen_mode = ScreenModes.SYSTEM _system_stats = {} _timebased_progress = False ##~~ StartupPlugin mixin def on_after_startup(self): """ StartupPlugin lifecycle hook, called after Octoprint startup is complete """ self.setup_display() self.setup_gpio() self.configure_gpio() self.clear_display() self.check_system_stats() self.start_system_timer() self.update_ui() self.start_display_timer() ##~~ ShutdownPlugin mixin def on_shutdown(self): """ ShutdownPlugin lifecycle hook, called before Octoprint shuts down """ self.stop_display_timer() self.clear_display() self.clean_gpio() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): """ EventHandlerPlugin lifecycle hook, called whenever an event is fired """ # self._logger.info("on_event: %s", event) self.set_printer_state(event) # Connectivity if event == Events.DISCONNECTED: self._screen_mode = ScreenModes.SYSTEM if event in (Events.CONNECTED, Events.CONNECTING, Events.CONNECTIVITY_CHANGED, Events.DISCONNECTING): self.update_ui() # Print start display if event == Events.PRINT_STARTED: self._screen_mode = ScreenModes.PRINT self.update_ui() # Print end states if event in (Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_CANCELLING): self._displaylayerprogress_current_height = -1.0 self._displaylayerprogress_current_layer = -1 self._displaylayerprogress_total_height = -1.0 self._displaylayerprogress_total_layer = -1 self.update_ui() # Print limbo states if event in (Events.PRINT_PAUSED, Events.PRINT_RESUMED): self.update_ui() # Mid-print states if event in (Events.Z_CHANGE, Events.PRINTER_STATE_CHANGED): self.update_ui() # Get progress information from DisplayLayerProgress plugin if event in ("DisplayLayerProgress_heightChanged", "DisplayLayerProgress_layerChanged"): if payload.get('currentHeight') != "-": self._displaylayerprogress_current_height = float(payload.get('currentHeight')) else: self._displaylayerprogress_current_height = -1.0 if payload.get('currentLayer') != "-": self._displaylayerprogress_current_layer = int(payload.get('currentLayer')) else: self._displaylayerprogress_current_layer = -1 self._displaylayerprogress_total_height = float(payload.get('totalHeight')) self._displaylayerprogress_total_layer = int(payload.get('totalLayer')) self.update_ui() ##~~ ProgressPlugin mixin def on_print_progress(self, storage, path, progress): """ ProgressPlugin lifecycle hook, called when print progress changes, at most in 1% incremements """ self.update_ui() def on_slicing_progress(self, slicer, source_location, source_path, destination_location, destination_path, progress): """ ProgressPlugin lifecycle hook, called whenever slicing progress changes, at most in 1% increments """ self._logger.info("on_slicing_progress: %s", progress) # TODO: Handle slicing progress bar self.update_ui() ##~~ TemplatePlugin mixin def get_template_configs(self): """ TemplatePlugin lifecycle hook, called to get templated settings """ return [dict(type="settings", custom_bindings=False)] ##~~ SettingsPlugin mixin def initialize(self): """ Prepare variables for setting modification detection """ self._debounce = int(self._settings.get(["debounce"])) self._display_init = False self._display_timeout_option = int(self._settings.get(["display_timeout_option"])) self._display_timeout_time = int(self._settings.get(["display_timeout_time"])) self._eta_strftime = str(self._settings.get(["eta_strftime"])) self._gpio_init = False self._i2c_address = str(self._settings.get(["i2c_address"])) self._image_rotate = bool(self._settings.get(["image_rotate"])) self._pin_cancel = int(self._settings.get(["pin_cancel"])) self._pin_mode = int(self._settings.get(["pin_mode"])) self._pin_pause = int(self._settings.get(["pin_pause"])) self._pin_play = int(self._settings.get(["pin_play"])) self._progress_on_top = bool(self._settings.get(["progress_on_top"])) self._screen_mode = ScreenModes.SYSTEM self._last_debounce = self._debounce self._last_display_timeout_option = self._display_timeout_option self._last_display_timeout_time = self._display_timeout_time self._last_i2c_address = self._i2c_address self._last_image_rotate = False self._last_pin_cancel = self._pin_cancel self._last_pin_mode = self._pin_mode self._last_pin_pause = self._pin_pause self._last_pin_play = self._pin_play self._timebased_progress = bool(self._settings.get(["timebased_progress"])) def get_settings_defaults(self): """ SettingsPlugin lifecycle hook, called to get default settings """ return dict( debounce = 250, # Debounce 250ms display_timeout_option = -1, # Default is never display_timeout_time = 5, # Default is 5 minutes eta_strftime = "%-m/%d %-I:%M%p", # Default is month/day hour:minute + AM/PM i2c_address = "0x3c", # Default is hex address 0x3c image_rotate = False, # Default if False (no rotation) pin_cancel = -1, # Default is disabled pin_mode = -1, # Default is disabled pin_pause = -1, # Default is disabled pin_play = -1, # Default is disabled progress_on_top = False, # Default is disabled timebased_progress = False, # Default is disabled ) def on_settings_save(self, data): """ SettingsPlugin lifecycle hook, called when settings are saved """ octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._debounce = int(self._settings.get(["debounce"])) self._display_timeout_option = int(self._settings.get(["display_timeout_option"])) self._display_timeout_time = int(self._settings.get(["display_timeout_time"])) self._eta_strftime = str(self._settings.get(["eta_strftime"])) self._i2c_address = str(self._settings.get(["i2c_address"])) self._image_rotate = bool(self._settings.get(["image_rotate"])) self._pin_cancel = int(self._settings.get(["pin_cancel"])) self._pin_mode = int(self._settings.get(["pin_mode"])) self._pin_pause = int(self._settings.get(["pin_pause"])) self._pin_play = int(self._settings.get(["pin_play"])) pins_updated = 0 try: if self._i2c_address.lower() != self._last_i2c_address.lower() or \ self._image_rotate != self._last_image_rotate: self.clear_display() self._display_init = False self._last_i2c_address = self._i2c_address self._last_image_rotate = self._image_rotate self.setup_display() self.clear_display() self.check_system_stats() self.start_system_timer() self._screen_mode = ScreenModes.SYSTEM self.update_ui() if self._pin_cancel != self._last_pin_cancel: pins_updated = pins_updated + 1 self.clean_single_gpio(self._last_pin_cancel) if self._pin_mode != self._last_pin_mode: pins_updated = pins_updated + 2 self.clean_single_gpio(self._last_pin_mode) if self._pin_pause != self._last_pin_pause: pins_updated = pins_updated + 4 self.clean_single_gpio(self._last_pin_pause) if self._pin_play != self._last_pin_play: pins_updated = pins_updated + 8 self.clean_single_gpio(self._last_pin_play) self._gpio_init = False self.setup_gpio() if pins_updated == (pow(2, 4) - 1) or self._debounce != self._last_debounce: self.configure_gpio() pins_updated = 0 if pins_updated >= pow(2, 3): self.configure_single_gpio(self._pin_play) pins_updated = pins_updated - pow(2, 3) if pins_updated >= pow(2, 2): self.configure_single_gpio(self._pin_pause) pins_updated = pins_updated - pow(2, 2) if pins_updated >= pow(2, 1): self.configure_single_gpio(self._pin_mode) pins_updated = pins_updated - pow(2, 1) if pins_updated >= pow(2, 0): self.configure_single_gpio(self._pin_cancel) pins_updated = pins_updated - pow(2, 0) if pins_updated > 0: self.log_error("Something went wrong counting updated GPIO pins") if self._display_timeout_option != self._last_display_timeout_option or \ self._display_timeout_time != self._last_display_timeout_time: self.start_display_timer(self._display_timeout_time != self._last_display_timeout_time) self._last_debounce = self._debounce self._last_display_timeout_option = self._display_timeout_option self._last_display_timeout_time = self._display_timeout_time self._last_pin_cancel = self._pin_cancel self._last_pin_mode = self._pin_mode self._last_pin_play = self._pin_play self._last_pin_pause = self._pin_pause except Exception as ex: self.log_error(ex) pass ##~~ Softwareupdate hook def get_update_information(self): """ Softwareupdate hook, standard library hook to handle software update and plugin version info """ # Define the configuration for your plugin to use with the Software Update # Plugin here. See https://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html # for details. return dict( display_panel=dict( displayName="OctoPrint Micro Panel", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-DisplayPanel", current=self._plugin_version, # update method: pip pip="https://github.com/sethvoltz/OctoPrint-DisplayPanel/archive/{target_version}.zip" ) ) ##~~ Helpers def bcm2board(self, bcm_pin): """ Function to translate bcm pin to board pin """ board_pin = -1 if bcm_pin != -1: _bcm2board = [ -1, -1, -1, 7, 29, 31, -1, -1, -1, -1, -1, 32, 33, -1, -1, 36, 11, 12, 35, 38, 40, 15, 16, 18, 22, 37, 13 ] board_pin=_bcm2board[bcm_pin-1] return board_pin def start_system_timer(self): """ Function to check system stats periodically """ self._check_system_timer = RepeatedTimer(5, self.check_system_stats, None, None, True) self._check_system_timer.start() def check_system_stats(self): """ Function to collect general system stats about the underlying system(s). Called by the system check timer on a regular basis. This function should remain small, performant and not block. """ if self._screen_mode == ScreenModes.SYSTEM: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) self._system_stats['ip'] = s.getsockname()[0] s.close() self._system_stats['load'] = psutil.getloadavg() self._system_stats['memory'] = psutil.virtual_memory() self._system_stats['disk'] = shutil.disk_usage('/') # disk percentage = 100 * used / (used + free) self.update_ui() elif self._screen_mode == ScreenModes.PRINTER: # Just update the UI, the printer mode will take care of itself self.update_ui() def setup_display(self): """ Intialize display """ try: self.i2c = busio.I2C(SCL, SDA) self.disp = adafruit_ssd1306.SSD1306_I2C(128, 64, self.i2c, addr=int(self._i2c_address,0)) self._logger.info("Setting display to I2C address %s", self._i2c_address) self._display_init = True self.font = ImageFont.load_default() self.width = self.disp.width self.height = self.disp.height self.image = Image.new("1", (self.width, self.height)) self.draw = ImageDraw.Draw(self.image) self._screen_mode = ScreenModes.SYSTEM except Exception as ex: self.log_error(ex) pass def setup_gpio(self): """ Setup GPIO to use BCM pin numbering, unless already setup in which case fall back and update globals to reflect change. """ self.BCM_PINS = { self._pin_mode: 'mode', self._pin_cancel: 'cancel', self._pin_play: 'play', self._pin_pause: 'pause' } self.BOARD_PINS = { self.bcm2board(self._pin_mode) : 'mode', self.bcm2board(self._pin_cancel): 'cancel', self.bcm2board(self._pin_play): 'play', self.bcm2board(self._pin_pause): 'pause' } self.input_pinset = self.BCM_PINS try: current_mode = GPIO.getmode() set_mode = GPIO.BCM if current_mode is None: GPIO.setmode(set_mode) self.input_pinset = self.BCM_PINS self._logger.info("Setting GPIO mode to BCM numbering") elif current_mode != set_mode: GPIO.setmode(current_mode) self.input_pinset = self.BOARD_PINS self._logger.info("GPIO mode was already set, adapting to use BOARD numbering") GPIO.setwarnings(False) self._gpio_init = True except Exception as ex: self.log_error(ex) pass def configure_gpio(self): """ Setup the GPIO pins to handle the buttons as inputs with built-in pull-up resistors """ if self._gpio_init: for gpio_pin in self.input_pinset: self.configure_single_gpio(gpio_pin) def configure_single_gpio(self, gpio_pin): """ Setup the GPIO pins to handle the buttons as inputs with built-in pull-up resistors """ if self._gpio_init: try: if gpio_pin != -1: GPIO.setup(gpio_pin, GPIO.IN, GPIO.PUD_UP) GPIO.remove_event_detect(gpio_pin) self._logger.info("Adding GPIO event detect on pin %s with edge: FALLING", gpio_pin) GPIO.add_event_detect(gpio_pin, GPIO.FALLING, callback=self.handle_gpio_event, bouncetime=self._debounce) except Exception as ex: self.log_error(ex) def clean_gpio(self): """ Remove event detection and clean up for all pins (`mode`, `cancel`, `play` and `pause`) """ if self._gpio_init: for gpio_pin in self.input_pinset: self.clean_single_gpio(gpio_pin) def clean_single_gpio(self, gpio_pin): """ Remove event detection and clean up for all pins (`mode`, `cancel`, `play` and `pause`) """ if self._gpio_init: if gpio_pin!=-1: try: GPIO.remove_event_detect(gpio_pin) except Exception as ex: self.log_error(ex) pass try: GPIO.cleanup(gpio_pin) except Exception as ex: self.log_error(ex) pass self._logger.info("Removed GPIO pin %s", gpio_pin) def handle_gpio_event(self, channel): """ Event callback for GPIO event, called from `add_event_detect` setup in `configure_gpio` """ try: if channel in self.input_pinset: if self._display_timeout_active: self.start_display_timer() return else: self.start_display_timer() label = self.input_pinset[channel] if label == 'cancel': self.try_cancel() else: if self._cancel_timer is not None: self.clear_cancel() else: if label == 'mode': self.next_mode() if label == 'play': self.try_play() if label == 'pause': self.try_pause() except Exception as ex: self.log_error(ex) pass def next_mode(self): """ Go to the next screen mode """ self._screen_mode = self._screen_mode.next() self.update_ui() def try_cancel(self): """ First click, confirm cancellation. Second click, trigger cancel """ if not self._printer.is_printing() and not self._printer.is_paused(): return if self._cancel_timer: # GPIO can double-trigger sometimes, check if this is too fast and ignore if (time.time() - self._cancel_requested_at) < 1: return # Cancel has been tried, run a cancel self.clear_cancel() self._printer.cancel_print() else: # First press self._cancel_requested_at = time.time() self._cancel_timer = RepeatedTimer(10, self.clear_cancel, run_first=False) self._cancel_timer.start() self.update_ui() def clear_cancel(self): """ Clear a pending cancel, something else was clicked or a timeout occured """ if self._cancel_timer: self._cancel_timer.cancel() self._cancel_timer = None self._cancel_requested_at = 0 self.update_ui() def try_play(self): """ If possible, play or resume a print """ # TODO: If a print is queued up and ready, start it if not self._printer.is_paused(): return self._printer.resume_print() def try_pause(self): """ If possible, pause a running print """ if not self._printer.is_printing(): return self._printer.pause_print() def clear_display(self): """ Clear the OLED display completely. Used at startup and shutdown to ensure a blank screen """ if self._display_init: self.disp.fill(0) self.disp.show() def set_printer_state(self, event): """ Set printer state based on latest event """ if event in (Events.DISCONNECTED, Events.CONNECTED, Events.PRINT_STARTED, Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_PAUSED, Events.PRINT_RESUMED): if event == Events.DISCONNECTED: self._printer_state = 0 if event in (Events.CONNECTED, Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_PAUSED): self._printer_state = 1 if event in (Events.PRINT_STARTED, Events.PRINT_RESUMED): self._printer_state = 2 if self._printer_state != self._last_printer_state: self.start_display_timer(True) self._last_printer_state = self._printer_state return def start_display_timer(self, reconfigure=False): """ Start timer for display timeout """ do_reset = False if self._display_timeout_timer is not None: if reconfigure: self.stop_display_timer() else: do_reset = True if do_reset: self._display_timeout_timer.reset() else: if self._printer_state <= self._display_timeout_option: self._display_timeout_timer = ResettableTimer(self._display_timeout_time * 60, self.trigger_display_timeout, [True], None, True) self._display_timeout_timer.start() if self._display_timeout_active: self.trigger_display_timeout(False) return def stop_display_timer(self): """ Stop timer for display timeout """ if self._display_timeout_timer is not None: self._display_timeout_timer.cancel() self._display_timeout_timer = None return def trigger_display_timeout(self, activate): """ Set display off on activate == True and on on activate == False """ self._display_timeout_active = activate if self._display_init: if activate: self.stop_display_timer() self.disp.poweroff() else: self.disp.poweron() return def update_ui(self): """ Update the on-screen UI based on the current screen mode and printer status """ if self._display_init: try: current_data = self._printer.get_current_data() if self._cancel_timer is not None and current_data['state']['flags']['cancelling'] is False: self.update_ui_cancel_confirm() elif self._screen_mode == ScreenModes.SYSTEM: self.update_ui_system() elif self._screen_mode == ScreenModes.PRINTER: self.update_ui_printer() elif self._screen_mode == ScreenModes.PRINT: self.update_ui_print(current_data) self.update_ui_bottom(current_data) # Display image. if self._image_rotate: self.disp.image(self.image.rotate(angle=180)) else: self.disp.image(self.image) self.disp.show() except Exception as ex: self.log_error(ex) def update_ui_cancel_confirm(self): """ Show a confirmation message that a cancel print has been requested """ top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) if self._display_init: try: self.draw.rectangle((0, top, self.width, bottom), fill=0) display_string = "Cancel Print?" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 0), display_string, font=self.font, fill=255) display_string = "Press 'X' to confirm" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 9), display_string, font=self.font, fill=255) display_string = "Press any button or" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 18), display_string, font=self.font, fill=255) display_string = "wait 10 sec to escape" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 27), display_string, font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_system(self): """ Update three-fourths of the screen with system stats collected by the timed collector """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) # Draw a black filled box to clear the image. self.draw.rectangle((0, top, self.width, bottom), fill=0) try: mem = self._system_stats['memory'] disk = self._system_stats['disk'] # Center IP ip = self._system_stats['ip'] tWidth, tHeight = self.draw.textsize(ip) self.draw.text((left + ((self.width - tWidth) / 2), top + offset + 0), ip, font=self.font, fill=255) # System Stats self.draw.text((left, top + offset + 9), "L: %s, %s, %s" % self._system_stats['load'], font=self.font, fill=255) self.draw.text((left, top + offset + 18), "M: %s/%s MB %s%%" % (int(mem.used/1048576), int(mem.total/1048576), mem.percent), font=self.font, fill=255) self.draw.text((left, top + offset + 27), "D: %s/%s GB %s%%" % (int(disk.used/1073741824), int((disk.used+disk.total)/1073741824), int(10000*disk.used/(disk.used+disk.free))/100), font=self.font, fill=255) except: self.draw.text((left, top + offset + 9), "Gathering System Stats", font=self.font, fill=255) def update_ui_printer(self): """ Update three-fourths of the screen with stats about the printer, such as temperatures """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) try: self.draw.rectangle((0, top, self.width, bottom), fill=0) self.draw.text((left, top + offset + 0), "Printer Temperatures", font=self.font, fill=255) if self._printer.get_current_connection()[0] == "Closed": self.draw.text((left, top + offset + 9), "Head: no printer", font=self.font, fill=255) self.draw.text((left, top + offset + 18), " Bed: no printer", font=self.font, fill=255) else: temperatures = self._printer.get_current_temperatures() tool = temperatures['tool0'] or None bed = temperatures['bed'] or None self.draw.text((left, top + offset + 9), "Head: %s / %s\xb0C" % (tool['actual'], tool['target']), font=self.font, fill=255) self.draw.text((left, top + offset + 18), " Bed: %s / %s\xb0C" % (bed['actual'], bed['target']), font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_print(self, current_data): """ Update three-fourths of the screen with information about the current ongoing print """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) try: self.draw.rectangle((0, top, self.width, bottom), fill=0) self.draw.text((left, top + offset + 0), "State: %s" % (self._printer.get_state_string()), font=self.font, fill=255) if current_data['job']['file']['name']: file_name = current_data['job']['file']['name'] self.draw.text((left, top + offset + 9), "File: %s" % (file_name), font=self.font, fill=255) print_time = self._get_time_from_seconds(current_data['progress']['printTime'] or 0) self.draw.text((left, top + offset + 18), "Time: %s" % (print_time), font=self.font, fill=255) filament = current_data['job']['filament']['tool0'] if "tool0" in current_data['job']['filament'] else current_data['job']['filament'] filament_length = self.float_count_formatter((filament['length'] or 0) / 1000, 3) filament_mass = self.float_count_formatter(filament['volume'] or 0, 3) self.draw.text((left, top + offset + 27), "Filament: %sm/%scm3" % (filament_length, filament_mass), font=self.font, fill=255) # Display height if information available from DisplayLayerProgress plugin height = "{:>5.1f}/{:>5.1f}".format(float(self._displaylayerprogress_current_height), float(self._displaylayerprogress_total_height)) layer = "{:>4d}/{:>4d}".format(self._displaylayerprogress_current_layer, self._displaylayerprogress_total_layer) height_text = "" if self._displaylayerprogress_current_height != -1.0 and self._displaylayerprogress_current_layer != -1: height_text = layer + ";" + height elif self._displaylayerprogress_current_layer != -1: height_text = layer elif self._displaylayerprogress_current_height != -1.0: height_text = height self.draw.text((left, top + offset + 36), height_text, font=self.font, fill=255) else: self.draw.text((left, top + offset + 18), "Waiting for file...", font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_bottom(self, current_data): """ Update one-fourths of the screen with persistent information about the current print """ if self._display_init: top = (self.height - self._colored_strip_height) * int(not self._progress_on_top) bottom = self.height - ((self.height - self._colored_strip_height) * int(self._progress_on_top)) left = 0 try: # Clear area self.draw.rectangle((0, top, self.width, bottom), fill=0) if self._printer.get_current_connection()[0] == "Closed": # Printer isn't connected display_string = "Printer Not Connected" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['paused'] or current_data['state']['flags']['pausing']: # Printer paused display_string = "Paused" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['cancelling']: # Printer paused display_string = "Cancelling" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['ready'] and (current_data['progress']['completion'] or 0) < 100: # Printer connected, not printing display_string = "Waiting For Job" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) else: percentage = int(current_data['progress']['completion'] or 0) # Calculate progress from time if current_data['progress']['printTime'] and self._timebased_progress: percentage = int((current_data['progress']['printTime'] or 0) / ((current_data['progress']['printTime'] or 0) + current_data['progress']['printTimeLeft']) * 100) time_left = current_data['progress']['printTimeLeft'] or 0 # Progress bar self.draw.rectangle((0, top + 0, self.width - 1, top + 5), fill=0, outline=255, width=1) bar_width = int((self.width - 5) * percentage / 100) self.draw.rectangle((2, top + 2, bar_width, top + 3), fill=255, outline=255, width=1) # Percentage and ETA self.draw.text((0, top + 5), "%s%%" % (percentage), font=self.font, fill=255) eta = time.strftime(self._eta_strftime, time.localtime(time.time() + time_left)) eta_width = self.draw.textsize(eta, font=self.font)[0] self.draw.text((self.width - eta_width, top + 5), eta, font=self.font, fill=255) except Exception as ex: self.log_error(ex) # Taken from tpmullan/OctoPrint-DetailedProgress def _get_time_from_seconds(self, seconds): hours = 0 minutes = 0 if seconds >= 3600: hours = int(seconds / 3600) seconds = seconds % 3600 if seconds >= 60: minutes = int(seconds / 60) seconds = seconds % 60 return self._etl_format.format(**locals()) def float_count_formatter(self, number, max_chars): """ Show decimals up to a max number of characters, then flips over and rounds to integer """ int_part = "%i" % round(number) if len(int_part) >= max_chars - 1: return int_part elif len("%f" % number) <= max_chars: return "%f" % number else: return "{num:0.{width}f}".format(num=number, width=len(int_part) - 1) def log_error(self, ex): """ Helper function for more complete logging on exceptions """ template = "An exception of type {0} occurred on {1}. Arguments: {2!r}" message = template.format(type(ex).__name__, inspect.currentframe().f_code.co_name, ex.args) self._logger.warn(message)
class tplinksmartplugPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.ProgressPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.tplinksmartplug") self._tplinksmartplug_logger = logging.getLogger("octoprint.plugins.tplinksmartplug.debug") self.abortTimeout = 0 self._timeout_value = None self._abort_timer = None self._countdown_active = False self.print_job_power = 0.0 self.print_job_started = False self._waitForHeaters = False self._waitForTimelapse = False self._timelapse_active = False self._skipIdleTimer = False self.powerOffWhenIdle = False self._idleTimer = None ##~~ StartupPlugin mixin def on_startup(self, host, port): # setup customized logger from octoprint.logging.handlers import CleaningTimedRotatingFileHandler tplinksmartplug_logging_handler = CleaningTimedRotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="debug"), when="D", backupCount=3) tplinksmartplug_logging_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) tplinksmartplug_logging_handler.setLevel(logging.DEBUG) self._tplinksmartplug_logger.addHandler(tplinksmartplug_logging_handler) self._tplinksmartplug_logger.setLevel(logging.DEBUG if self._settings.get_boolean(["debug_logging"]) else logging.INFO) self._tplinksmartplug_logger.propagate = False self.db_path = os.path.join(self.get_plugin_data_folder(),"energy_data.db") if not os.path.exists(self.db_path): db = sqlite3.connect(self.db_path) cursor = db.cursor() cursor.execute('''CREATE TABLE energy_data(id INTEGER PRIMARY KEY, ip TEXT, timestamp TEXT, current REAL, power REAL, total REAL, voltage REAL)''') db.commit() db.close() self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._tplinksmartplug_logger.debug("abortTimeout: %s" % self.abortTimeout) self.powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) self._tplinksmartplug_logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._tplinksmartplug_logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._tplinksmartplug_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"]) self._tplinksmartplug_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) self._start_idle_timer() def on_after_startup(self): self._logger.info("TPLinkSmartplug loaded!") if self._settings.get(["pollingEnabled"]): self.poll_status = RepeatedTimer(int(self._settings.get(["pollingInterval"]))*60, self.check_statuses) self.poll_status.start() ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( debug_logging = False, arrSmartplugs = [], pollingInterval = 15, pollingEnabled = False, thermal_runaway_monitoring = False, thermal_runaway_max_bed = 0, thermal_runaway_max_extruder = 0, event_on_error_monitoring = False, event_on_disconnect_monitoring = False, cost_rate = 0, abortTimeout = 30, powerOffWhenIdle = False, idleTimeout = 30, idleIgnoreCommands = 'M105', idleTimeoutWaitTemp = 50 ) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) old_polling_value = self._settings.get_boolean(["pollingEnabled"]) old_polling_timer = self._settings.get(["pollingInterval"]) old_powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) old_idleTimeout = self._settings.get_int(["idleTimeout"]) old_idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) old_idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"]) if self.powerOffWhenIdle != old_powerOffWhenIdle: self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle == True: self._tplinksmartplug_logger.debug("Settings saved, Automatic Power Off Endabled, starting idle timer...") self._start_idle_timer() new_debug_logging = self._settings.get_boolean(["debug_logging"]) new_polling_value = self._settings.get_boolean(["pollingEnabled"]) new_polling_timer = self._settings.get(["pollingInterval"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._tplinksmartplug_logger.setLevel(logging.DEBUG) else: self._tplinksmartplug_logger.setLevel(logging.INFO) if old_polling_value != new_polling_value or old_polling_timer != new_polling_timer: if self.poll_status: self.poll_status.cancel() if new_polling_value: self.poll_status = RepeatedTimer(int(self._settings.get(["pollingInterval"]))*60, self.check_statuses) self.poll_status.start() def get_settings_version(self): return 11 def on_settings_migrate(self, target, current=None): if current is None or current < 5: # Reset plug settings to defaults. self._tplinksmartplug_logger.debug("Resetting arrSmartplugs for tplinksmartplug settings.") self._settings.set(['arrSmartplugs'], self.get_settings_defaults()["arrSmartplugs"]) elif current == 6: # Loop through plug array and set emeter to None arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["emeter"] = None arrSmartplugs_new.append(plug) self._tplinksmartplug_logger.info("Updating plug array, converting") self._tplinksmartplug_logger.info(self._settings.get(['arrSmartplugs'])) self._tplinksmartplug_logger.info("to") self._tplinksmartplug_logger.info(arrSmartplugs_new) self._settings.set(["arrSmartplugs"],arrSmartplugs_new) elif current == 7: # Loop through plug array and set emeter to None arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["emeter"] = dict(get_realtime = False) arrSmartplugs_new.append(plug) self._tplinksmartplug_logger.info("Updating plug array, converting") self._tplinksmartplug_logger.info(self._settings.get(['arrSmartplugs'])) self._tplinksmartplug_logger.info("to") self._tplinksmartplug_logger.info(arrSmartplugs_new) self._settings.set(["arrSmartplugs"],arrSmartplugs_new) if current is not None and current < 9: arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["thermal_runaway"] = False arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"],arrSmartplugs_new) if current is not None and current < 10: arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["event_on_error"] = False plug["event_on_disconnect"] = False arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"],arrSmartplugs_new) if current is not None and current < 11: arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["automaticShutdownEnabled"] = False arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"],arrSmartplugs_new) ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/jquery-ui.min.js","js/knockout-sortable.js","js/fontawesome-iconpicker.js","js/ko.iconpicker.js","js/tplinksmartplug.js","js/knockout-bootstrap.min.js","js/ko.observableDictionary.js","js/plotly-latest.min.js"], css=["css/font-awesome.min.css","css/font-awesome-v4-shims.min.css","css/fontawesome-iconpicker.css","css/tplinksmartplug.css"] ) ##~~ TemplatePlugin mixin def get_template_configs(self): #templates_to_load = [dict(type="navbar", custom_bindings=True),dict(type="settings", custom_bindings=True),dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: show_sidebar()", template="tplinksmartplug_sidebar.jinja2", template_header="tplinksmartplug_sidebar_header.jinja2"),dict(type="tab", custom_bindings=True)] templates_to_load = [dict(type="navbar", custom_bindings=True),dict(type="settings", custom_bindings=True),dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: arrSmartplugs().length > 0", template="tplinksmartplug_sidebar.jinja2", template_header="tplinksmartplug_sidebar_header.jinja2"),dict(type="tab", custom_bindings=True, data_bind="visible: show_sidebar()", template="tplinksmartplug_tab.jinja2")] return templates_to_load ##~~ ProgressPlugin mixin def on_print_progress(self, storage, path, progress): self._tplinksmartplug_logger.debug("Checking statuses during print progress (%s)." % progress) self.check_statuses() self._plugin_manager.send_plugin_message(self._identifier, dict(updatePlot=True)) if self.powerOffWhenIdle == True and not (self._skipIdleTimer == True): self._tplinksmartplug_logger.debug("Resetting idle timer during print progress (%s)..." % progress) self._waitForHeaters = False self._reset_idle_timer() ##~~ SimpleApiPlugin mixin def turn_on(self, plugip): self._tplinksmartplug_logger.debug("Turning on %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if "/" in plugip: plug_ip, plug_num = plugip.split("/") else: plug_ip = plugip plug_num = -1 if plug["useCountdownRules"] and int(plug["countdownOnDelay"]) > 0: self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'),plug_ip, plug_num) chk = self.lookup(self.sendCommand(json.loads('{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":1,"name":"turn on"}}}' % plug["countdownOnDelay"]),plug_ip,plug_num),*["count_down","add_rule","err_code"]) if chk == 0: self._countdown_active = True c = threading.Timer(int(plug["countdownOnDelay"])+3,self._plugin_manager.send_plugin_message,[self._identifier, dict(check_status=True,ip=plugip)]) c.start() else: turn_on_cmnd = dict(system=dict(set_relay_state=dict(state=1))) chk = self.lookup(self.sendCommand(turn_on_cmnd,plug_ip,plug_num),*["system","set_relay_state","err_code"]) self._tplinksmartplug_logger.debug(chk) if chk == 0: if plug["autoConnect"] and self._printer.is_closed_or_error(): c = threading.Timer(int(plug["autoConnectDelay"]),self._printer.connect) c.start() if plug["sysCmdOn"]: t = threading.Timer(int(plug["sysCmdOnDelay"]),os.system,args=[plug["sysRunCmdOn"]]) t.start() if self.powerOffWhenIdle == True and plug["automaticShutdownEnabled"] == True: self._tplinksmartplug_logger.debug("Resetting idle timer since plug %s was just turned on." % plugip) self._waitForHeaters = False self._reset_idle_timer() return self.check_status(plugip) def turn_off(self, plugip): self._tplinksmartplug_logger.debug("Turning off %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if "/" in plugip: plug_ip, plug_num = plugip.split("/") else: plug_ip = plugip plug_num = -1 if plug["useCountdownRules"] and int(plug["countdownOffDelay"]) > 0: self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'),plug_ip,plug_num) chk = self.lookup(self.sendCommand(json.loads('{"count_down":{"add_rule":{"enable":1,"delay":%s,"act":0,"name":"turn off"}}}' % plug["countdownOffDelay"]),plug_ip,plug_num),*["count_down","add_rule","err_code"]) if chk == 0: self._countdown_active = True c = threading.Timer(int(plug["countdownOffDelay"])+3,self._plugin_manager.send_plugin_message,[self._identifier, dict(check_status=True,ip=plugip)]) c.start() if plug["sysCmdOff"]: t = threading.Timer(int(plug["sysCmdOffDelay"]),os.system,args=[plug["sysRunCmdOff"]]) t.start() if plug["autoDisconnect"]: self._printer.disconnect() time.sleep(int(plug["autoDisconnectDelay"])) if not plug["useCountdownRules"]: turn_off_cmnd = dict(system=dict(set_relay_state=dict(state=0))) chk = self.lookup(self.sendCommand(turn_off_cmnd,plug_ip,plug_num),*["system","set_relay_state","err_code"]) self._tplinksmartplug_logger.debug(chk) if chk == 0: return self.check_status(plugip) def check_statuses(self): for plug in self._settings.get(["arrSmartplugs"]): chk = self.check_status(plug["ip"]) self._plugin_manager.send_plugin_message(self._identifier, chk) def check_status(self, plugip): self._tplinksmartplug_logger.debug("Checking status of %s." % plugip) if plugip != "": emeter_data = None today = datetime.today() check_status_cmnd = dict(system = dict(get_sysinfo = dict())) plug_ip = plugip.split("/") self._tplinksmartplug_logger.debug(check_status_cmnd) if len(plug_ip) == 2: response = self.sendCommand(check_status_cmnd, plug_ip[0], plug_ip[1]) timer_chk = self.lookup(response, *["system","get_sysinfo","children"])[int(plug_ip[1])]["on_time"] else: response = self.sendCommand(check_status_cmnd, plug_ip[0]) timer_chk = self.deep_get(response,["system","get_sysinfo","on_time"], default=0) if timer_chk == 0 and self._countdown_active: self._tplinksmartplug_logger.debug("Clearing previously active countdown timer flag") self._countdown_active = False self._tplinksmartplug_logger.debug(self.deep_get(response,["system","get_sysinfo","feature"], default="")) if "ENE" in self.deep_get(response,["system","get_sysinfo","feature"], default=""): # if "ENE" in self.lookup(response, *["system","get_sysinfo","feature"]): emeter_data_cmnd = dict(emeter = dict(get_realtime = dict())) if len(plug_ip) == 2: check_emeter_data = self.sendCommand(emeter_data_cmnd, plug_ip[0], plug_ip[1]) else: check_emeter_data = self.sendCommand(emeter_data_cmnd, plug_ip[0]) if self.lookup(check_emeter_data, *["emeter","get_realtime"]): emeter_data = check_emeter_data["emeter"] if "voltage_mv" in emeter_data["get_realtime"]: v = emeter_data["get_realtime"]["voltage_mv"] / 1000.0 elif "voltage" in emeter_data["get_realtime"]: v = emeter_data["get_realtime"]["voltage"] else: v = "" if "current_ma" in emeter_data["get_realtime"]: c = emeter_data["get_realtime"]["current_ma"] / 1000.0 elif "current" in emeter_data["get_realtime"]: c = emeter_data["get_realtime"]["current"] else: c = "" if "power_mw" in emeter_data["get_realtime"]: p = emeter_data["get_realtime"]["power_mw"] / 1000.0 elif "power" in emeter_data["get_realtime"]: p = emeter_data["get_realtime"]["power"] else: p = "" if "total_wh" in emeter_data["get_realtime"]: t = emeter_data["get_realtime"]["total_wh"] / 1000.0 elif "total" in emeter_data["get_realtime"]: t = emeter_data["get_realtime"]["total"] else: t = "" db = sqlite3.connect(self.db_path) cursor = db.cursor() cursor.execute('''INSERT INTO energy_data(ip, timestamp, current, power, total, voltage) VALUES(?,?,?,?,?,?)''', [plugip,today.isoformat(' '),c,p,t,v]) db.commit() db.close() if len(plug_ip) == 2: chk = self.lookup(response,*["system","get_sysinfo","children"]) if chk: chk = chk[int(plug_ip[1])]["state"] else: chk = self.lookup(response,*["system","get_sysinfo","relay_state"]) if chk == 1: return dict(currentState="on",emeter=emeter_data,ip=plugip) elif chk == 0: return dict(currentState="off",emeter=emeter_data,ip=plugip) else: self._tplinksmartplug_logger.debug(response) return dict(currentState="unknown",emeter=emeter_data,ip=plugip) def get_api_commands(self): return dict( turnOn=["ip"], turnOff=["ip"], checkStatus=["ip"], getEnergyData=["ip"], enableAutomaticShutdown=[], disableAutomaticShutdown=[], abortAutomaticShutdown=[]) def on_api_get(self, request): self._tplinksmartplug_logger.debug(request.args) if request.args.get("checkStatus"): response = self.check_status(request.args.get("checkStatus")) return flask.jsonify(response) def on_api_command(self, command, data): if not user_permission.can(): return flask.make_response("Insufficient rights", 403) if command == 'turnOn': response = self.turn_on("{ip}".format(**data)) self._plugin_manager.send_plugin_message(self._identifier, response) elif command == 'turnOff': response = self.turn_off("{ip}".format(**data)) self._plugin_manager.send_plugin_message(self._identifier, response) elif command == 'checkStatus': response = self.check_status("{ip}".format(**data)) elif command == 'getEnergyData': db = sqlite3.connect(self.db_path) cursor = db.cursor() cursor.execute('''SELECT timestamp, current, power, total, voltage FROM energy_data WHERE ip=? ORDER BY timestamp DESC LIMIT ?,?''', (data["ip"],data["record_offset"],data["record_limit"])) response = {'energy_data' : cursor.fetchall()} db.close() self._tplinksmartplug_logger.debug(response) #SELECT * FROM energy_data WHERE ip = '192.168.0.102' LIMIT 0,30 elif command == 'enableAutomaticShutdown': self.powerOffWhenIdle = True elif command == 'disableAutomaticShutdown': self.powerOffWhenIdle = False elif command == 'abortAutomaticShutdown': if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None for plug in self._settings.get(["arrSmartplugs"]): if plug["useCountdownRules"] and int(plug["countdownOffDelay"]) > 0: if "/" in plug["ip"]: plug_ip, plug_num = plug["ip"].split("/") else: plug_ip = plug["ip"] plug_num = -1 self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'),plug_ip,plug_num) self._tplinksmartplug_logger.debug("Cleared countdown rules for %s" % plug["ip"]) self._tplinksmartplug_logger.debug("Power off aborted.") self._tplinksmartplug_logger.debug("Restarting idle timer.") self._reset_idle_timer() else: response = dict(ip = data.ip, currentState = "unknown") if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown": self._tplinksmartplug_logger.debug("Automatic power off setting changed: %s" % self.powerOffWhenIdle) self._settings.set_boolean(["powerOffWhenIdle"], self.powerOffWhenIdle) self._settings.save() #eventManager().fire(Events.SETTINGS_UPDATED) if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown" or command == "abortAutomaticShutdown": self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) else: return flask.jsonify(response) ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): # Error Event if event == Events.ERROR and self._settings.getBoolean(["event_on_error_monitoring"]) == True: self._tplinksmartplug_logger.debug("powering off due to %s event." % event) for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_error"] == True: self._tplinksmartplug_logger.debug("powering off %s due to %s event." % (plug["ip"], event)) response = self.turn_off(plug["ip"]) if response["currentState"] == "off": self._plugin_manager.send_plugin_message(self._identifier, response) # Disconnected Event if event == Events.DISCONNECTED and self._settings.getBoolean(["event_on_disconnect_monitoring"]) == True: self._tplinksmartplug_logger.debug("powering off due to %s event." % event) for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_disconnect"] == True: self._tplinksmartplug_logger.debug("powering off %s due to %s event." % (plug["ip"], event)) response = self.turn_off(plug["ip"]) if response["currentState"] == "off": self._plugin_manager.send_plugin_message(self._identifier, response) # Client Opened Event if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) return # Cancelled Print Interpreted Event if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error(): self._tplinksmartplug_logger.debug("Print cancelled, reseting job_power to 0") self.print_job_power = 0.0 self.print_job_started = False return # Print Started Event if event == Events.PRINT_STARTED and self._settings.getFloat(["cost_rate"]) > 0: self.print_job_started = True self._tplinksmartplug_logger.debug(payload.get("path", None)) for plug in self._settings.get(["arrSmartplugs"]): status = self.check_status(plug["ip"]) self.print_job_power -= float(self.deep_get(status,["emeter","get_realtime","total_wh"], default=0)) / 1000 self.print_job_power -= float(self.deep_get(status,["emeter","get_realtime","total"], default=0)) self._tplinksmartplug_logger.debug(self.print_job_power) if event == Events.PRINT_STARTED and self.powerOffWhenIdle == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._tplinksmartplug_logger.debug("Power off aborted because starting new print.") if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if event == Events.PRINT_STARTED and self._countdown_active: for plug in self._settings.get(["arrSmartplugs"]): if plug["useCountdownRules"] and int(plug["countdownOffDelay"]) > 0: if "/" in plug["ip"]: plug_ip, plug_num = plug["ip"].split("/") else: plug_ip = plug["ip"] plug_num = -1 self.sendCommand(json.loads('{"count_down":{"delete_all_rules":null}}'),plug_ip,plug_num) self._tplinksmartplug_logger.debug("Cleared countdown rules for %s" % plug["ip"]) # Print Done Event if event == Events.PRINT_DONE and self.print_job_started: self._tplinksmartplug_logger.debug(payload) for plug in self._settings.get(["arrSmartplugs"]): status = self.check_status(plug["ip"]) self.print_job_power += float(self.deep_get(status,["emeter","get_realtime","total_wh"], default=0)) / 1000 self.print_job_power += float(self.deep_get(status,["emeter","get_realtime","total"], default=0)) self._tplinksmartplug_logger.debug(self.print_job_power) hours = (payload.get("time", 0)/60)/60 self._tplinksmartplug_logger.debug("hours: %s" % hours) power_used = self.print_job_power * hours self._tplinksmartplug_logger.debug("power used: %s" % power_used) power_cost = power_used * self._settings.getFloat(["cost_rate"]) self._tplinksmartplug_logger.debug("power total cost: %s" % power_cost) self._storage_interface = self._file_manager._storage(payload.get("origin", "local")) self._storage_interface.set_additional_metadata(payload.get("path"), "statistics", dict(lastPowerCost=dict(_default=float('{:.4f}'.format(power_cost)))), merge=True) self.print_job_power = 0.0 self.print_job_started = False if self.powerOffWhenIdle == True and event == Events.MOVIE_RENDERING: self._tplinksmartplug_logger.debug("Timelapse generation started: %s" % payload.get("movie_basename", "")) self._timelapse_active = True if self._timelapse_active and event == Events.MOVIE_DONE or event == Events.MOVIE_FAILED: self._tplinksmartplug_logger.debug("Timelapse generation finished: %s. Return Code: %s" % (payload.get("movie_basename", ""), payload.get("returncode", "completed"))) self._timelapse_active = False ##~~ Idle Timeout def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._waitForTimelapse: return if self._printer.is_printing() or self._printer.is_paused(): return self._tplinksmartplug_logger.debug("Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._tplinksmartplug_logger.debug("Heaters below temperature.") if self._wait_for_timelapse(): self._timer_start() else: self._tplinksmartplug_logger.debug("Aborted power off due to activity.") ##~~ Timelapse Monitoring def _wait_for_timelapse(self): self._waitForTimelapse = True self._tplinksmartplug_logger.debug("Checking timelapse status before shutting off power...") while True: if not self._waitForTimelapse: return False if not self._timelapse_active: self._waitForTimelapse = False return True self._tplinksmartplug_logger.debug("Waiting for timelapse before shutting off power...") time.sleep(5) ##~~ Temperature Cooldown def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._tplinksmartplug_logger.debug("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._tplinksmartplug_logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._tplinksmartplug_logger.debug("Heater %s = %sC" % (heater,temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._tplinksmartplug_logger.debug("Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer def _timer_start(self): if self._abort_timer is not None: return self._tplinksmartplug_logger.debug("Starting abort power off timer.") self._timeout_value = self.abortTimeout self._abort_timer = RepeatedTimer(1, self._timer_task) self._abort_timer.start() def _timer_task(self): if self._timeout_value is None: return self._timeout_value -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): self._tplinksmartplug_logger.debug("Automatically powering off enabled plugs.") for plug in self._settings.get(['arrSmartplugs']): if plug.get("automaticShutdownEnabled", False): response = self.turn_off("{ip}".format(**plug)) self._plugin_manager.send_plugin_message(self._identifier, response) ##~~ Utilities def _get_device_id(self, plugip): response = self._settings.get([plugip]) if not response: check_status_cmnd = dict(system = dict(get_sysinfo = dict())) plug_ip = plugip.split("/") self._tplinksmartplug_logger.debug(check_status_cmnd) plug_data = self.sendCommand(check_status_cmnd, plug_ip[0]) if len(plug_ip) == 2: response = self.deep_get(plug_data,["system","get_sysinfo","children"], default=False) if response: response = response[int(plug_ip[1])]["id"] else: response = self.deep_get(response,["system","get_sysinfo","deviceId"]) if response: self._settings.set([plugip],response) self._settings.save() self._tplinksmartplug_logger.debug("get_device_id response: %s" % response) return response def deep_get(self, d, keys, default=None): """ Example: d = {'meta': {'status': 'OK', 'status_code': 200}} deep_get(d, ['meta', 'status_code']) # => 200 deep_get(d, ['garbage', 'status_code']) # => None deep_get(d, ['meta', 'garbage'], default='-') # => '-' """ assert type(keys) is list if d is None: return default if not keys: return d return self.deep_get(d.get(keys[0]), keys[1:], default) def lookup(self, dic, key, *keys): if keys: return self.lookup(dic.get(key, {}), *keys) return dic.get(key) def plug_search(self, list, key, value): for item in list: if item[key] == value.strip(): return item def encrypt(self, string): key = 171 result = b"\0\0\0" + bytes([len(string)]) for i in bytes(string.encode('latin-1')): a = key ^ i key = a result += bytes([a]) return result def decrypt(self, string): key = 171 result = b"" for i in bytes(string): a = key ^ i key = i result += bytes([a]) return result.decode('latin-1') def sendCommand(self, cmd, plugip, plug_num = -1): commands = {'info' : '{"system":{"get_sysinfo":{}}}', 'on' : '{"system":{"set_relay_state":{"state":1}}}', 'off' : '{"system":{"set_relay_state":{"state":0}}}', 'cloudinfo': '{"cnCloud":{"get_info":{}}}', 'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}', 'time' : '{"time":{"get_time":{}}}', 'schedule' : '{"schedule":{"get_rules":{}}}', 'countdown': '{"count_down":{"get_rules":{}}}', 'antitheft': '{"anti_theft":{"get_rules":{}}}', 'reboot' : '{"system":{"reboot":{"delay":1}}}', 'reset' : '{"system":{"reset":{"delay":1}}}' } if re.search('/\d+$', plugip): self._tplinksmartplug_logger.exception("Internal error passing unsplit %s", plugip) # try to connect via ip address try: socket.inet_aton(plugip) ip = plugip self._tplinksmartplug_logger.debug("IP %s is valid." % plugip) except socket.error: # try to convert hostname to ip self._tplinksmartplug_logger.debug("Invalid ip %s trying hostname." % plugip) try: ip = socket.gethostbyname(plugip) self._tplinksmartplug_logger.debug("Hostname %s is valid." % plugip) except (socket.herror, socket.gaierror): self._tplinksmartplug_logger.debug("Invalid hostname %s." % plugip) return {"system":{"get_sysinfo":{"relay_state":3}},"emeter":{"err_code": True}} if int(plug_num) >= 0: plug_ip_num = plugip + "/" + plug_num cmd["context"] = dict(child_ids = [self._get_device_id(plug_ip_num)]) try: self._tplinksmartplug_logger.debug("Sending command %s to %s" % (cmd,plugip)) sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_tcp.connect((ip, 9999)) sock_tcp.send(self.encrypt(json.dumps(cmd))) data = sock_tcp.recv(1024) len_data = unpack('>I', data[0:4]) while (len(data) - 4) < len_data[0]: data = data + sock_tcp.recv(1024) sock_tcp.close() self._tplinksmartplug_logger.debug(self.decrypt(data)) return json.loads(self.decrypt(data[4:])) except socket.error: self._tplinksmartplug_logger.debug("Could not connect to %s." % plugip) return {"system":{"get_sysinfo":{"relay_state":3}},"emeter":{"err_code": True}} ##~~ Gcode processing hook def gcode_turn_off(self, plug): if plug["warnPrinting"] and self._printer.is_printing(): self._tplinksmartplug_logger.debug("Not powering off %s because printer is printing." % plug["label"]) else: chk = self.turn_off(plug["ip"]) self._plugin_manager.send_plugin_message(self._identifier, chk) def gcode_turn_on(self, plug): chk = self.turn_on(plug["ip"]) self._plugin_manager.send_plugin_message(self._identifier, chk) def processGCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if gcode: if cmd.startswith("M80"): plugip = re.sub(r'^M80\s?', '', cmd) self._tplinksmartplug_logger.debug("Received M80 command, attempting power on of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if plug and plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOnDelay"]),self.gcode_turn_on,[plug]) t.start() return elif cmd.startswith("M81"): plugip = re.sub(r'^M81\s?', '', cmd) self._tplinksmartplug_logger.debug("Received M81 command, attempting power off of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if plug and plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOffDelay"]),self.gcode_turn_off,[plug]) t.start() return elif self.powerOffWhenIdle and not (gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() else: return def processAtCommand(self, comm_instance, phase, command, parameters, tags=None, *args, **kwargs): self._logger.info(command) self._logger.info(parameters) if command == "TPLINKON": plugip = parameters self._tplinksmartplug_logger.debug("Received @TPLINKON command, attempting power on of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if plug and plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOnDelay"]),self.gcode_turn_on,[plug]) t.start() return None if command == "TPLINKOFF": plugip = parameters self._tplinksmartplug_logger.debug("Received TPLINKOFF command, attempting power off of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]),"ip",plugip) self._tplinksmartplug_logger.debug(plug) if plug and plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOffDelay"]),self.gcode_turn_off,[plug]) t.start() return None ##~~ Temperatures received hook def check_temps(self, parsed_temps): thermal_runaway_triggered = False for k, v in parsed_temps.items(): if k == "B" and v[0] > int(self._settings.get(["thermal_runaway_max_bed"])): self._tplinksmartplug_logger.debug("Max bed temp reached, shutting off plugs.") thermal_runaway_triggered = True if k.startswith("T") and v[0] > int(self._settings.get(["thermal_runaway_max_extruder"])): self._tplinksmartplug_logger.debug("Extruder max temp reached, shutting off plugs.") thermal_runaway_triggered = True if thermal_runaway_triggered == True: for plug in self._settings.get(['arrSmartplugs']): if plug["thermal_runaway"] == True: response = self.turn_off(plug["ip"]) if response["currentState"] == "off": self._plugin_manager.send_plugin_message(self._identifier, response) def monitor_temperatures(self, comm, parsed_temps): if self._settings.get(["thermal_runaway_monitoring"]): # Run inside it's own thread to prevent communication blocking t = threading.Timer(0,self.check_temps,[parsed_temps]) t.start() return parsed_temps ##~~ Softwareupdate hook def get_update_information(self): return dict( tplinksmartplug=dict( displayName="TP-Link Smartplug", displayVersion=self._plugin_version, type="github_release", user="******", repo="OctoPrint-TPLinkSmartplug", current=self._plugin_version, pip="https://github.com/jneilliii/OctoPrint-TPLinkSmartplug/archive/{target_version}.zip" ) )
class PSUControl(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): try: global GPIO import RPi.GPIO as GPIO self._hasGPIO = True except (ImportError, RuntimeError): self._hasGPIO = False self._pin_to_gpio_rev1 = [ -1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev2 = [ -1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev3 = [ -1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21 ] self.GPIOMode = '' self.switchingMethod = '' self.onoffGPIOPin = 0 self.invertonoffGPIOPin = False self.onGCodeCommand = '' self.offGCodeCommand = '' self.onSysCommand = '' self.offSysCommand = '' self.enablePseudoOnOff = False self.pseudoOnGCodeCommand = '' self.pseudoOffGCodeCommand = '' self.postOnDelay = 0.0 self.autoOn = False self.autoOnTriggerGCodeCommands = '' self._autoOnTriggerGCodeCommandsArray = [] self.enablePowerOffWarningDialog = True self.powerOffWhenIdle = False self.idleTimeout = 0 self.idleIgnoreCommands = '' self._idleIgnoreCommandsArray = [] self.idleTimeoutWaitTemp = 0 self.disconnectOnPowerOff = False self.autoConnectOnPowerON = False self.autoConnectWaitTimeout = 0.0 self.autoConnectPort = '' self.autoConnectBaudrate = '' self.autoConnectPrinterProfile = '' self.sensingMethod = '' self.senseGPIOPin = 0 self.invertsenseGPIOPin = False self.senseGPIOPinPUD = '' self.senseSystemCommand = '' self.isPSUOn = False self._noSensing_isPSUOn = False self._check_psu_state_thread = None self._check_psu_state_event = threading.Event() self._idleTimer = None self._waitForHeaters = False self._skipIdleTimer = False self._configuredGPIOPins = [] def on_settings_initialized(self): self.GPIOMode = self._settings.get(["GPIOMode"]) self._logger.debug("GPIOMode: %s" % self.GPIOMode) self.switchingMethod = self._settings.get(["switchingMethod"]) self._logger.debug("switchingMethod: %s" % self.switchingMethod) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self._logger.debug("onoffGPIOPin: %s" % self.onoffGPIOPin) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self._logger.debug("invertonoffGPIOPin: %s" % self.invertonoffGPIOPin) self.onGCodeCommand = self._settings.get(["onGCodeCommand"]) self._logger.debug("onGCodeCommand: %s" % self.onGCodeCommand) self.offGCodeCommand = self._settings.get(["offGCodeCommand"]) self._logger.debug("offGCodeCommand: %s" % self.offGCodeCommand) self.onSysCommand = self._settings.get(["onSysCommand"]) self._logger.debug("onSysCommand: %s" % self.onSysCommand) 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.disconnectOnPowerOff = self._settings.get_boolean( ["disconnectOnPowerOff"]) self._logger.debug("disconnectOnPowerOff: %s" % self.disconnectOnPowerOff) self.autoConnectOnPowerON = self._settings.get_boolean( ["autoConnectOnPowerON"]) self._logger.debug("autoConnectOnPowerON: %s" % self.autoConnectOnPowerON) self.autoConnectWaitTimeout = self._settings.get_float( ["autoConnectWaitTimeout"]) self._logger.debug("autoConnectWaitTimeout: %s" % self.autoConnectWaitTimeout) self.autoConnectPort = self._settings.get(["autoConnectPort"]) self._logger.debug("autoConnectPort: %s" % self.autoConnectPort) self.autoConnectBaudrate = self._settings.get(["autoConnectBaudrate"]) self._logger.debug("autoConnectBaudrate: %s" % self.autoConnectBaudrate) self.autoConnectPrinterProfile = self._settings.get( ["autoConnectPrinterProfile"]) self._logger.debug("autoConnectPrinterProfile: %s" % self.autoConnectPrinterProfile) 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) self.autoOnTriggerGCodeCommands = self._settings.get( ["autoOnTriggerGCodeCommands"]) self._autoOnTriggerGCodeCommandsArray = self.autoOnTriggerGCodeCommands.split( ',') self._logger.debug("autoOnTriggerGCodeCommands: %s" % self.autoOnTriggerGCodeCommands) self.enablePowerOffWarningDialog = self._settings.get_boolean( ["enablePowerOffWarningDialog"]) self._logger.debug("enablePowerOffWarningDialog: %s" % self.enablePowerOffWarningDialog) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) if self.switchingMethod == 'GCODE': self._logger.info("Using G-Code Commands for On/Off") elif self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") elif self.switchingMethod == 'SYSTEM': self._logger.info("Using System Commands for On/Off") if self.sensingMethod == 'INTERNAL': self._logger.info("Using internal tracking for PSU on/off state.") elif self.sensingMethod == 'GPIO': self._logger.info("Using GPIO for tracking PSU on/off state.") elif self.sensingMethod == 'SYSTEM': self._logger.info( "Using System Commands for tracking PSU on/off state.") if self.switchingMethod == 'GPIO' or self.sensingMethod == 'GPIO': self._configure_gpio() self._check_psu_state_thread = threading.Thread( target=self._check_psu_state) self._check_psu_state_thread.daemon = True self._check_psu_state_thread.start() self._start_idle_timer() def _gpio_board_to_bcm(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio[pin] def _gpio_bcm_to_board(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio.index(pin) def _gpio_get_pin(self, pin): if (GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BOARD') or (GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BCM'): return pin elif GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BCM': return self._gpio_bcm_to_board(pin) elif GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BOARD': return self._gpio_board_to_bcm(pin) else: return 0 def _configure_gpio(self): if not self._hasGPIO: self._logger.error("RPi.GPIO is required.") return self._logger.info("Running RPi.GPIO version %s" % GPIO.VERSION) if GPIO.VERSION < "0.6": self._logger.error("RPi.GPIO version 0.6.0 or greater required.") GPIO.setwarnings(False) for pin in self._configuredGPIOPins: self._logger.debug("Cleaning up pin %s" % pin) try: GPIO.cleanup(self._gpio_get_pin(pin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._configuredGPIOPins = [] if GPIO.getmode() is None: if self.GPIOMode == 'BOARD': GPIO.setmode(GPIO.BOARD) elif self.GPIOMode == 'BCM': GPIO.setmode(GPIO.BCM) else: return 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, pull_up_down=pudsenseGPIOPin) self._configuredGPIOPins.append(self.senseGPIOPin) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") self._logger.info("Configuring GPIO for pin %s" % self.onoffGPIOPin) try: 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): self._check_psu_state_event.set() def _check_psu_state(self): while True: old_isPSUOn = self.isPSUOn if self.sensingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Polling PSU state...") r = 0 try: r = GPIO.input(self._gpio_get_pin(self.senseGPIOPin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._logger.debug("Result: %s" % r) if r == 1: new_isPSUOn = True elif r == 0: 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, shell=True) self._logger.debug( "Sensing system command executed. PID=%s, Command=%s" % (p.pid, self.senseSystemCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Sensing system command returned: %s" % r) if r == 0: new_isPSUOn = True elif r == 1: new_isPSUOn = False self.isPSUOn = new_isPSUOn elif self.sensingMethod == 'INTERNAL': self.isPSUOn = self._noSensing_isPSUOn else: return self._logger.debug("isPSUOn: %s" % self.isPSUOn) if (old_isPSUOn != self.isPSUOn) and self.isPSUOn: self._start_idle_timer() elif (old_isPSUOn != self.isPSUOn) and not self.isPSUOn: self._stop_idle_timer() self._plugin_manager.send_plugin_message( self._identifier, dict(hasGPIO=self._hasGPIO, isPSUOn=self.isPSUOn)) self._check_psu_state_event.wait(5) self._check_psu_state_event.clear() def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle and self.isPSUOn: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._printer.is_printing() or self._printer.is_paused(): return self._logger.info( "Idle timeout reached after %s minute(s). Turning heaters off prior to shutting off PSU." % self.idleTimeout) if self._wait_for_heaters(): self._logger.info("Heaters below temperature.") self.turn_psu_off() else: self._logger.info("Aborted PSU shut down due to activity.") def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater in heaters.keys(): if float(heaters.get(heater)["target"]) != 0: self._logger.info("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater in heaters.keys(): if heater == 'bed': continue temp = float(heaters.get(heater)["actual"]) self._logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._logger.info( "Waiting for heaters(%s) before shutting off PSU..." % ', '.join(heaters_above_waittemp)) 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() if self.powerOffWhenIdle and self.isPSUOn and not self._skipIdleTimer: if not (gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_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") if self.switchingMethod == 'GCODE': self._logger.debug("Switching PSU On Using GCODE: %s" % self.onGCodeCommand) self._printer.commands(self.onGCodeCommand) elif self.switchingMethod == 'SYSTEM': self._logger.debug("Switching PSU On Using SYSTEM: %s" % self.onSysCommand) p = subprocess.Popen(self.onSysCommand, shell=True) self._logger.debug( "On system command executed. PID=%s, Command=%s" % (p.pid, self.onSysCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("On system command returned: %s" % r) elif self.switchingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Switching PSU On Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = GPIO.HIGH else: pin_output = GPIO.LOW try: GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.sensingMethod not in ('GPIO', 'SYSTEM'): self._noSensing_isPSUOn = True if self.autoConnectOnPowerON: self._logger.debug( "Automatically connect Printer on Power on: %s") time.sleep(0.1 + float(self.autoConnectWaitTimeout)) self._logger.debug("Connect after Sleep") if self.autoConnectPort == '': self.autoConnectPort = None if self.autoConnectBaudrate == '': self.autoConnectBaudrate = None if self.autoConnectPrinterProfile == '': self.autoConnectPrinterProfile = None self._printer.connect(self.autoConnectPort, self.autoConnectBaudrate, self.autoConnectPrinterProfile) time.sleep(0.1 + self.postOnDelay) self.check_psu_state() def turn_psu_off(self): if self.switchingMethod == 'GCODE' or self.switchingMethod == 'GPIO' or self.switchingMethod == 'SYSTEM': self._logger.info("Switching PSU Off") if self.switchingMethod == 'GCODE': self._logger.debug("Switching PSU Off Using GCODE: %s" % self.offGCodeCommand) self._printer.commands(self.offGCodeCommand) elif self.switchingMethod == 'SYSTEM': self._logger.debug("Switching PSU Off Using SYSTEM: %s" % self.offSysCommand) p = subprocess.Popen(self.offSysCommand, shell=True) self._logger.debug( "Off system command executed. PID=%s, Command=%s" % (p.pid, self.offSysCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Off system command returned: %s" % r) elif self.switchingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Switching PSU Off Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = GPIO.LOW else: pin_output = GPIO.HIGH try: GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.disconnectOnPowerOff: self._printer.disconnect() if self.sensingMethod not in ('GPIO', 'SYSTEM'): self._noSensing_isPSUOn = False time.sleep(0.1) self.check_psu_state() def get_api_commands(self): return dict(turnPSUOn=[], turnPSUOff=[], togglePSU=[], getPSUState=[]) def on_api_command(self, command, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if command == 'turnPSUOn': 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(GPIOMode='BOARD', switchingMethod='GCODE', onoffGPIOPin=0, invertonoffGPIOPin=False, onGCodeCommand='M80', offGCodeCommand='M81', onSysCommand='', offSysCommand='', enablePseudoOnOff=False, pseudoOnGCodeCommand='M80', pseudoOffGCodeCommand='M81', postOnDelay=0.0, disconnectOnPowerOff=False, autoConnectOnPowerON=False, autoConnectWaitTimeout=15, autoConnectPort='', autoConnectBaudrate='', autoConnectPrinterProfile='', sensingMethod='INTERNAL', senseGPIOPin=0, invertsenseGPIOPin=False, senseGPIOPinPUD='', senseSystemCommand='', autoOn=False, autoOnTriggerGCodeCommands= "G0,G1,G2,G3,G10,G11,G28,G29,G32,M104,M106,M109,M140,M190", enablePowerOffWarningDialog=True, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50) def on_settings_save(self, data): old_GPIOMode = self.GPIOMode old_onoffGPIOPin = self.onoffGPIOPin 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"]) self.switchingMethod = self._settings.get(["switchingMethod"]) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self.onGCodeCommand = self._settings.get(["onGCodeCommand"]) 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.disconnectOnPowerOff = self._settings.get_boolean( ["disconnectOnPowerOff"]) self.autoConnectOnPowerON = self._settings.get_boolean( ["autoConnectOnPowerON"]) self.autoConnectWaitTimeout = self._settings.get( ["autoConnectWaitTimeout"]) self.autoConnectPort = self._settings.get(["autoConnectPort"]) self.autoConnectBaudrate = self._settings.get(["autoConnectBaudrate"]) self.autoConnectPrinterProfile = self._settings.get( ["autoConnectPrinterProfile"]) 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( ',') self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) 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_sensingMethod != self.sensingMethod or old_invertsenseGPIOPin != self.invertsenseGPIOPin or old_senseGPIOPinPUD != self.senseGPIOPinPUD or old_switchingMethod != self.switchingMethod) and (self.switchingMethod == 'GPIO' or self.sensingMethod == 'GPIO')): self._configure_gpio() self._start_idle_timer() def get_settings_version(self): return 3 def on_settings_migrate(self, target, current=None): if current is None or current < 2: # v2 changes names of settings variables to accomidate system commands. cur_switchingMethod = self._settings.get(["switchingMethod"]) if cur_switchingMethod is not None and cur_switchingMethod == "COMMAND": self._logger.info( "Migrating Setting: switchingMethod=COMMAND -> switchingMethod=GCODE" ) self._settings.set(["switchingMethod"], "GCODE") cur_onCommand = self._settings.get(["onCommand"]) if cur_onCommand is not None: self._logger.info( "Migrating Setting: onCommand={0} -> onGCodeCommand={0}". format(cur_onCommand)) self._settings.set(["onGCodeCommand"], cur_onCommand) self._settings.remove(["onCommand"]) cur_offCommand = self._settings.get(["offCommand"]) if cur_offCommand is not None: self._logger.info( "Migrating Setting: offCommand={0} -> offGCodeCommand={0}". format(cur_offCommand)) self._settings.set(["offGCodeCommand"], cur_offCommand) self._settings.remove(["offCommand"]) cur_autoOnCommands = self._settings.get(["autoOnCommands"]) if cur_autoOnCommands is not None: self._logger.info( "Migrating Setting: autoOnCommands={0} -> autoOnTriggerGCodeCommands={0}" .format(cur_autoOnCommands)) 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=True)] def get_assets(self): return { "js": ["js/psucontrol.js"], "less": ["less/psucontrol.less"], "css": ["css/psucontrol.min.css"] } def get_update_information(self): return dict(psucontrol=dict( displayName="PSU Control", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-PSUControl", current=self._plugin_version, # update method: pip w/ dependency links pip= "https://github.com/kantlivelong/OctoPrint-PSUControl/archive/{target_version}.zip" ))
class PSUoff(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): try: global GPIO import RPi.GPIO as GPIO self._hasGPIO = True except (ImportError, RuntimeError): self._hasGPIO = False self._pin_to_gpio_rev1 = [ -1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev2 = [ -1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev3 = [ -1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21 ] self.GPIOMode = '' self.onoffGPIOPin = 0 self.invertonoffGPIOPin = False self.autoOn = False self.enablePowerOffWarningDialog = True self.shutdownOnPowerOff = True self.powerOffWhenIdle = False self.idleTimeout = 0 self.idleIgnoreCommands = '' self._idleIgnoreCommandsArray = [] self.idleTimeoutWaitTemp = 0 self.isPSUOn = False self._idleTimer = None self._waitForHeaters = False self._skipIdleTimer = False self._configuredGPIOPins = [] self.isFirstRun = True ##~~ EventHandlerPlugin def on_event(self, event, payload): if event in ("PrintDone"): self.isFirstRun = False self._start_idle_timer() self._logger.debug("PrintDone Event") def on_settings_initialized(self): self.GPIOMode = self._settings.get(["GPIOMode"]) self._logger.debug("GPIOMode: %s" % self.GPIOMode) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self._logger.debug("onoffGPIOPin: %s" % self.onoffGPIOPin) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self._logger.debug("invertonoffGPIOPin: %s" % self.invertonoffGPIOPin) self.enablePowerOffWarningDialog = self._settings.get_boolean( ["enablePowerOffWarningDialog"]) self._logger.debug("enablePowerOffWarningDialog: %s" % self.enablePowerOffWarningDialog) self.shutdownOnPowerOff = self._settings.get_boolean( ["shutdownOnPowerOff"]) self._logger.debug("shutdownOnPowerOff: %s" % self.shutdownOnPowerOff) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) self._configure_gpio() #self._start_idle_timer() self._logger.debug("Start OK") def _gpio_board_to_bcm(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio[pin] def _gpio_bcm_to_board(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio.index(pin) def _gpio_get_pin(self, pin): if (GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BOARD') or (GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BCM'): return pin elif GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BCM': return self._gpio_bcm_to_board(pin) elif GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BOARD': return self._gpio_board_to_bcm(pin) else: return 0 def _configure_gpio(self): if not self._hasGPIO: self._logger.error("RPi.GPIO is required.") return self._logger.info("Running RPi.GPIO version %s" % GPIO.VERSION) if GPIO.VERSION < "0.6": self._logger.error("RPi.GPIO version 0.6.0 or greater required.") GPIO.setwarnings(False) for pin in self._configuredGPIOPins: self._logger.debug("Cleaning up pin %s" % pin) try: GPIO.cleanup(self._gpio_get_pin(pin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._configuredGPIOPins = [] if GPIO.getmode() is None: if self.GPIOMode == 'BOARD': GPIO.setmode(GPIO.BOARD) elif self.GPIOMode == 'BCM': GPIO.setmode(GPIO.BCM) else: return try: 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 _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() # Funkce vyvolaná po timeoutu od ResettableTimer def _idle_poweroff(self): if self.isFirstRun: self._logger.debug("cekani 0") return if not self.powerOffWhenIdle: self._logger.debug("cekani 1") return if self._waitForHeaters: self._logger.debug("cekani 2") return if self._printer.is_printing() or self._printer.is_paused(): self._logger.debug("cekani 3") return self._logger.info( "Idle timeout reached after %s minute(s). Turning heaters off prior to shutting off PSU." % self.idleTimeout) if self._wait_for_heaters(): self._logger.info("Heaters below temperature.") #TEST vypnutí se nespustí self.turn_psu_off() self.turn_psu_off() else: self._logger.info("Aborted PSU shut down due to activity.") def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._logger.info("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._logger.info( "Waiting for heaters(%s) before shutting off PSU..." % ', '.join(heaters_above_waittemp)) time.sleep(5) def hook_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): skipqueuing = false if gcode: if self.poweroffwhenidle and self.ispsuon and not self._skipidletimer: if not (gcode in self._idleignorecommandsarray): self._waitforheaters = false self._reset_idle_timer() if skipqueuing: return (none, ) def turn_psu_off(self): self._logger.info("Switching PSU Off") if not self._hasGPIO: return self._logger.debug("Switching PSU Off Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = GPIO.HIGH else: pin_output = GPIO.LOW try: GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) except (RuntimeError, ValueError) as e: self._logger.error(e) # vypnutí Raspberry if self.shutdownOnPowerOff: self._logger.info("Shutdown system") try: os.system("sudo shutdown -h now") except: e = sys.exc_info()[0] self._logger.error("Error executing shutdown command") self._noSensing_isPSUOn = False def get_api_commands(self): return dict(turnPSUOff=[], turnoffPSU=[], getPSUState=[]) def on_api_get(self, request): return self.on_api_command("getPSUState", []) def on_api_command(self, command, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if command == 'turnoffPSU': self.turn_psu_off() elif command == 'turnPSUOff': self.turn_psu_off() elif command == 'getPSUState': return jsonify(isPSUOn=self.isPSUOn) def get_settings_defaults(self): return dict(GPIOMode='BOARD', onoffGPIOPin=0, invertonoffGPIOPin=False, enablePowerOffWarningDialog=True, shutdownOnPowerOff=True, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50) def on_settings_save(self, data): old_GPIOMode = self.GPIOMode old_onoffGPIOPin = self.onoffGPIOPin octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.GPIOMode = self._settings.get(["GPIOMode"]) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self.shutdownOnPowerOff = self._settings.get_boolean( ["shutdownOnPowerOff"]) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self.enablePowerOffWarningDialog = self._settings.get_boolean( ["enablePowerOffWarningDialog"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._configure_gpio() #self._start_idle_timer() def get_settings_version(self): return 3 def on_settings_migrate(self, target, current=None): if current is None: current = 0 # zatim tu nic není def get_template_configs(self): return [dict(type="settings", custom_bindings=True)] def get_assets(self): return { "js": ["js/psuoff.js"], "less": ["less/psuoff.less"], "css": ["css/psuoff.min.css"] } def get_update_information(self): return dict(psuoff=dict( displayName="PSU Off", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-PSUoff", current=self._plugin_version, # update method: pip w/ dependency links pip= "https://github.com/tomasbubela/OctoPrint-PSUoff/archive/{target_version}.zip" ))
class PSUControl(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.WizardPlugin): def __init__(self): self._sub_plugins = dict() self._availableGPIODevices = self.get_gpio_devs() self.config = dict() self._autoOnTriggerGCodeCommandsArray = [] self._idleIgnoreCommandsArray = [] self._check_psu_state_thread = None self._check_psu_state_event = threading.Event() self._idleTimer = None self._waitForHeaters = False self._skipIdleTimer = False self._configuredGPIOPins = {} self._noSensing_isPSUOn = False self.isPSUOn = False def get_settings_defaults(self): return dict( GPIODevice = '', switchingMethod = 'GCODE', onoffGPIOPin = 0, invertonoffGPIOPin = False, onGCodeCommand = 'M80', offGCodeCommand = 'M81', onSysCommand = '', offSysCommand = '', switchingPlugin = '', enablePseudoOnOff = False, pseudoOnGCodeCommand = 'M80', pseudoOffGCodeCommand = 'M81', postOnDelay = 0.0, connectOnPowerOn = False, disconnectOnPowerOff = False, sensingMethod = 'INTERNAL', senseGPIOPin = 0, sensePollingInterval = 5, invertsenseGPIOPin = False, senseGPIOPinPUD = '', senseSystemCommand = '', sensingPlugin = '', autoOn = False, autoOnTriggerGCodeCommands = "G0,G1,G2,G3,G10,G11,G28,G29,G32,M104,M106,M109,M140,M190", enablePowerOffWarningDialog = True, powerOffWhenIdle = False, idleTimeout = 30, idleIgnoreCommands = 'M105', idleTimeoutWaitTemp = 50, turnOnWhenApiUploadPrint = False, turnOffWhenError = False ) def on_settings_initialized(self): scripts = self._settings.listScripts("gcode") if not "psucontrol_post_on" in scripts: self._settings.saveScript("gcode", "psucontrol_post_on", u'') if not "psucontrol_pre_off" in scripts: self._settings.saveScript("gcode", "psucontrol_pre_off", u'') self.reload_settings() def reload_settings(self): for k, v in self.get_settings_defaults().items(): if type(v) == str: v = self._settings.get([k]) elif type(v) == int: v = self._settings.get_int([k]) elif type(v) == float: v = self._settings.get_float([k]) elif type(v) == bool: v = self._settings.get_boolean([k]) self.config[k] = v self._logger.debug("{}: {}".format(k, v)) if self.config['switchingMethod'] == 'GPIO' and not HAS_GPIO: self._logger.error("Unable to use GPIO for switchingMethod.") self.config['switchingMethod'] = '' if self.config['sensingMethod'] == 'GPIO' and not HAS_GPIO: self._logger.error("Unable to use GPIO for sensingMethod.") self.config['sensingMethod'] = '' if self.config['enablePseudoOnOff'] and self.config['switchingMethod'] == 'GCODE': self._logger.warning("Pseudo On/Off cannot be used in conjunction with GCODE switching. Disabling.") self.config['enablePseudoOnOff'] = False self._autoOnTriggerGCodeCommandsArray = self.config['autoOnTriggerGCodeCommands'].split(',') self._idleIgnoreCommandsArray = self.config['idleIgnoreCommands'].split(',') def on_after_startup(self): if self.config['switchingMethod'] == 'GPIO' or self.config['sensingMethod'] == 'GPIO': self.configure_gpio() self._check_psu_state_thread = threading.Thread(target=self._check_psu_state) self._check_psu_state_thread.daemon = True self._check_psu_state_thread.start() self._start_idle_timer() def get_gpio_devs(self): return sorted(glob.glob('/dev/gpiochip*')) def cleanup_gpio(self): for k, pin in self._configuredGPIOPins.items(): self._logger.debug("Cleaning up {} pin {}".format(k, pin.name)) try: pin.close() except Exception: self._logger.exception( "Exception while cleaning up {} pin {}.".format(k, pin.name) ) self._configuredGPIOPins = {} def configure_gpio(self): self._logger.info("Periphery version: {}".format(periphery.version)) if self.config['switchingMethod'] == 'GPIO': self._logger.info("Using GPIO for On/Off") self._logger.info("Configuring GPIO for pin {}".format(self.config['onoffGPIOPin'])) if not self.config['invertonoffGPIOPin']: initial_output = 'low' else: initial_output = 'high' try: pin = periphery.GPIO(self.config['GPIODevice'], self.config['onoffGPIOPin'], initial_output) self._configuredGPIOPins['switch'] = pin except Exception: self._logger.exception( "Exception while setting up GPIO pin {}".format(self.config['onoffGPIOPin']) ) if self.config['sensingMethod'] == 'GPIO': self._logger.info("Using GPIO sensing to determine PSU on/off state.") self._logger.info("Configuring GPIO for pin {}".format(self.config['senseGPIOPin'])) if not SUPPORTS_LINE_BIAS: if self.config['senseGPIOPinPUD'] != '': self._logger.warning("Kernel version 5.5 or greater required for GPIO bias. Using 'default'.") bias = "default" elif self.config['senseGPIOPinPUD'] == '': bias = "disable" elif self.config['senseGPIOPinPUD'] == 'PULL_UP': bias = "pull_up" elif self.config['senseGPIOPinPUD'] == 'PULL_DOWN': bias = "pull_down" else: bias = "default" try: pin = periphery.CdevGPIO(path=self.config['GPIODevice'], line=self.config['senseGPIOPin'], direction='in', bias=bias) self._configuredGPIOPins['sense'] = pin except Exception: self._logger.exception( "Exception while setting up GPIO pin {}".format(self.config['senseGPIOPin']) ) def _get_plugin_key(self, implementation): for k, v in self._plugin_manager.plugin_implementations.items(): if v == implementation: return k def register_plugin(self, implementation): k = self._get_plugin_key(implementation) self._logger.debug("Registering plugin - {}".format(k)) if k not in self._sub_plugins: self._logger.info("Registered plugin - {}".format(k)) self._sub_plugins[k] = implementation def check_psu_state(self): self._check_psu_state_event.set() def _check_psu_state(self): while True: old_isPSUOn = self.isPSUOn self._logger.debug("Polling PSU state...") if self.config['sensingMethod'] == 'GPIO': r = 0 try: r = self._configuredGPIOPins['sense'].read() except Exception: self._logger.exception("Exception while reading GPIO line") self._logger.debug("Result: {}".format(r)) new_isPSUOn = r ^ self.config['invertsenseGPIOPin'] self.isPSUOn = new_isPSUOn elif self.config['sensingMethod'] == 'SYSTEM': new_isPSUOn = False p = subprocess.Popen(self.config['senseSystemCommand'], shell=True) self._logger.debug("Sensing system command executed. PID={}, Command={}".format(p.pid, self.config['senseSystemCommand'])) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Sensing system command returned: {}".format(r)) if r == 0: new_isPSUOn = True elif r == 1: new_isPSUOn = False self.isPSUOn = new_isPSUOn elif self.config['sensingMethod'] == 'INTERNAL': self.isPSUOn = self._noSensing_isPSUOn elif self.config['sensingMethod'] == 'PLUGIN': p = self.config['sensingPlugin'] r = False if p not in self._sub_plugins: self._logger.error('Plugin {} is configured for sensing but it is not registered.'.format(p)) elif not hasattr(self._sub_plugins[p], 'get_psu_state'): self._logger.error('Plugin {} is configured for sensing but get_psu_state is not defined.'.format(p)) else: callback = self._sub_plugins[p].get_psu_state try: r = callback() except Exception: self._logger.exception( "Error while executing callback {}".format( callback ), extra={"callback": fqfn(callback)}, ) self.isPSUOn = r else: self.isPSUOn = False self._logger.debug("isPSUOn: {}".format(self.isPSUOn)) if (old_isPSUOn != self.isPSUOn): self._logger.debug("PSU state changed, firing psu_state_changed event.") event = Events.PLUGIN_PSUCONTROL_PSU_STATE_CHANGED self._event_bus.fire(event, payload=dict(isPSUOn=self.isPSUOn)) if (old_isPSUOn != self.isPSUOn) and self.isPSUOn: self._start_idle_timer() elif (old_isPSUOn != self.isPSUOn) and not self.isPSUOn: self._stop_idle_timer() self._plugin_manager.send_plugin_message(self._identifier, dict(isPSUOn=self.isPSUOn)) self._check_psu_state_event.wait(self.config['sensePollingInterval']) self._check_psu_state_event.clear() def _start_idle_timer(self): self._stop_idle_timer() if self.config['powerOffWhenIdle'] and self.isPSUOn: self._idleTimer = ResettableTimer(self.config['idleTimeout'] * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.config['powerOffWhenIdle']: return if self._waitForHeaters: return if self._printer.is_printing() or self._printer.is_paused(): return self._logger.info("Idle timeout reached after {} minute(s). Turning heaters off prior to shutting off PSU.".format(self.config['idleTimeout'])) if self._wait_for_heaters(): self._logger.info("Heaters below temperature.") self.turn_psu_off() else: self._logger.info("Aborted PSU shut down due to activity.") def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._logger.info("Turning off heater: {}".format(heater)) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._logger.debug("Heater {} already off.".format(heater)) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._logger.debug("Heater {} = {}C".format(heater, temp)) if temp > self.config['idleTimeoutWaitTemp']: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.config['idleTimeoutWaitTemp']: self._waitForHeaters = False return True self._logger.info("Waiting for heaters({}) before shutting off PSU...".format(', '.join(heaters_above_waittemp))) time.sleep(5) def hook_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): skipQueuing = False if not gcode: gcode = cmd.split(' ', 1)[0] if self.config['enablePseudoOnOff']: if gcode == self.config['pseudoOnGCodeCommand']: self.turn_psu_on() comm_instance._log("PSUControl: ok") skipQueuing = True elif gcode == self.config['pseudoOffGCodeCommand']: self.turn_psu_off() comm_instance._log("PSUControl: ok") skipQueuing = True if (not self.isPSUOn and self.config['autoOn'] and (gcode in self._autoOnTriggerGCodeCommandsArray)): self._logger.info("Auto-On - Turning PSU On (Triggered by {})".format(gcode)) self.turn_psu_on() if self.config['powerOffWhenIdle'] and self.isPSUOn and not self._skipIdleTimer: if not (gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() if skipQueuing: return (None,) def turn_psu_on(self): if self.config['switchingMethod'] in ['GCODE', 'GPIO', 'SYSTEM', 'PLUGIN']: self._logger.info("Switching PSU On") if self.config['switchingMethod'] == 'GCODE': self._logger.debug("Switching PSU On Using GCODE: {}".format(self.config['onGCodeCommand'])) self._printer.commands(self.config['onGCodeCommand']) elif self.config['switchingMethod'] == 'SYSTEM': self._logger.debug("Switching PSU On Using SYSTEM: {}".format(self.config['onSysCommand'])) p = subprocess.Popen(self.config['onSysCommand'], shell=True) self._logger.debug("On system command executed. PID={}, Command={}".format(p.pid, self.config['onSysCommand'])) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("On system command returned: {}".format(r)) elif self.config['switchingMethod'] == 'GPIO': self._logger.debug("Switching PSU On Using GPIO: {}".format(self.config['onoffGPIOPin'])) pin_output = bool(1 ^ self.config['invertonoffGPIOPin']) try: self._configuredGPIOPins['switch'].write(pin_output) except Exception : self._logger.exception("Exception while writing GPIO line") return elif self.config['switchingMethod'] == 'PLUGIN': p = self.config['switchingPlugin'] self._logger.debug("Switching PSU On Using PLUGIN: {}".format(p)) if p not in self._sub_plugins: self._logger.error('Plugin {} is configured for switching but it is not registered.'.format(p)) return elif not hasattr(self._sub_plugins[p], 'turn_psu_on'): self._logger.error('Plugin {} is configured for switching but turn_psu_on is not defined.'.format(p)) return else: callback = self._sub_plugins[p].turn_psu_on try: r = callback() except Exception: self._logger.exception( "Error while executing callback {}".format( callback ), extra={"callback": fqfn(callback)}, ) return if self.config['sensingMethod'] not in ('GPIO', 'SYSTEM', 'PLUGIN'): self._noSensing_isPSUOn = True time.sleep(0.1 + self.config['postOnDelay']) self.check_psu_state() if self.config['connectOnPowerOn'] and self._printer.is_closed_or_error(): self._printer.connect() time.sleep(0.1) if not self._printer.is_closed_or_error(): self._printer.script("psucontrol_post_on", must_be_set=False) def turn_psu_off(self): if self.config['switchingMethod'] in ['GCODE', 'GPIO', 'SYSTEM', 'PLUGIN']: if not self._printer.is_closed_or_error(): self._printer.script("psucontrol_pre_off", must_be_set=False) self._logger.info("Switching PSU Off") if self.config['switchingMethod'] == 'GCODE': self._logger.debug("Switching PSU Off Using GCODE: {}".format(self.config['offGCodeCommand'])) self._printer.commands(self.config['offGCodeCommand']) elif self.config['switchingMethod'] == 'SYSTEM': self._logger.debug("Switching PSU Off Using SYSTEM: {}".format(self.config['offSysCommand'])) p = subprocess.Popen(self.config['offSysCommand'], shell=True) self._logger.debug("Off system command executed. PID={}, Command={}".format(p.pid, self.config['offSysCommand'])) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Off system command returned: {}".format(r)) elif self.config['switchingMethod'] == 'GPIO': self._logger.debug("Switching PSU Off Using GPIO: {}".format(self.config['onoffGPIOPin'])) pin_output = bool(0 ^ self.config['invertonoffGPIOPin']) try: self._configuredGPIOPins['switch'].write(pin_output) except Exception: self._logger.exception("Exception while writing GPIO line") return elif self.config['switchingMethod'] == 'PLUGIN': p = self.config['switchingPlugin'] self._logger.debug("Switching PSU Off Using PLUGIN: {}".format(p)) if p not in self._sub_plugins: self._logger.error('Plugin {} is configured for switching but it is not registered.'.format(p)) return elif not hasattr(self._sub_plugins[p], 'turn_psu_off'): self._logger.error('Plugin {} is configured for switching but turn_psu_off is not defined.'.format(p)) return else: callback = self._sub_plugins[p].turn_psu_off try: r = callback() except Exception: self._logger.exception( "Error while executing callback {}".format( callback ), extra={"callback": fqfn(callback)}, ) return if self.config['disconnectOnPowerOff']: self._printer.disconnect() if self.config['sensingMethod'] not in ('GPIO', 'SYSTEM', 'PLUGIN'): self._noSensing_isPSUOn = False time.sleep(0.1) self.check_psu_state() def get_psu_state(self): return self.isPSUOn def turn_on_before_printing_after_upload(self): if ( self.config['turnOnWhenApiUploadPrint'] and not self.isPSUOn and flask.request.path.startswith('/api/files/') and flask.request.method == 'POST' and flask.request.values.get('print', 'false') in valid_boolean_trues): self.on_api_command("turnPSUOn", []) def on_event(self, event, payload): if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message(self._identifier, dict(isPSUOn=self.isPSUOn)) return elif event == Events.ERROR and self.config['turnOffWhenError']: self._logger.info("Firmware or communication error detected. Turning PSU Off") self.turn_psu_off() return def get_api_commands(self): return dict( turnPSUOn=[], turnPSUOff=[], togglePSU=[], getPSUState=[] ) def on_api_get(self, request): return self.on_api_command("getPSUState", []) def on_api_command(self, command, data): if command in ['turnPSUOn', 'turnPSUOff', 'togglePSU']: try: if not Permissions.PLUGIN_PSUCONTROL_CONTROL.can(): return make_response("Insufficient rights", 403) except: if not user_permission.can(): return make_response("Insufficient rights", 403) elif command in ['getPSUState']: try: if not Permissions.STATUS.can(): return make_response("Insufficient rights", 403) except: if not user_permission.can(): return make_response("Insufficient rights", 403) if command == 'turnPSUOn': 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 on_settings_save(self, data): if 'scripts_gcode_psucontrol_post_on' in data: script = data["scripts_gcode_psucontrol_post_on"] self._settings.saveScript("gcode", "psucontrol_post_on", u'' + script.replace("\r\n", "\n").replace("\r", "\n")) data.pop('scripts_gcode_psucontrol_post_on') if 'scripts_gcode_psucontrol_pre_off' in data: script = data["scripts_gcode_psucontrol_pre_off"] self._settings.saveScript("gcode", "psucontrol_pre_off", u'' + script.replace("\r\n", "\n").replace("\r", "\n")) data.pop('scripts_gcode_psucontrol_pre_off') old_config = self.config.copy() octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.reload_settings() #cleanup GPIO self.cleanup_gpio() #configure GPIO if self.config['switchingMethod'] == 'GPIO' or self.config['sensingMethod'] == 'GPIO': self.configure_gpio() self._start_idle_timer() def get_wizard_version(self): return 1 def is_wizard_required(self): return True def get_settings_version(self): return 4 def on_settings_migrate(self, target, current=None): if current is None: current = 0 if current < 2: # v2 changes names of settings variables to accomidate system commands. cur_switchingMethod = self._settings.get(["switchingMethod"]) if cur_switchingMethod is not None and cur_switchingMethod == "COMMAND": self._logger.info("Migrating Setting: switchingMethod=COMMAND -> switchingMethod=GCODE") self._settings.set(["switchingMethod"], "GCODE") cur_onCommand = self._settings.get(["onCommand"]) if cur_onCommand is not None: self._logger.info("Migrating Setting: onCommand={0} -> onGCodeCommand={0}".format(cur_onCommand)) self._settings.set(["onGCodeCommand"], cur_onCommand) self._settings.remove(["onCommand"]) cur_offCommand = self._settings.get(["offCommand"]) if cur_offCommand is not None: self._logger.info("Migrating Setting: offCommand={0} -> offGCodeCommand={0}".format(cur_offCommand)) self._settings.set(["offGCodeCommand"], cur_offCommand) self._settings.remove(["offCommand"]) cur_autoOnCommands = self._settings.get(["autoOnCommands"]) if cur_autoOnCommands is not None: self._logger.info("Migrating Setting: autoOnCommands={0} -> autoOnTriggerGCodeCommands={0}".format(cur_autoOnCommands)) 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"]) if current < 4: # v4 drops RPi.GPIO in favor of Python-Periphery. cur_GPIOMode = self._settings.get(["GPIOMode"]) cur_switchingMethod = self._settings.get(["switchingMethod"]) cur_sensingMethod = self._settings.get(["sensingMethod"]) cur_onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) cur_invertonoffGPIOPin = self._settings.get_boolean(["invertonoffGPIOPin"]) cur_senseGPIOPin = self._settings.get_int(["senseGPIOPin"]) cur_invertsenseGPIOPin = self._settings.get_boolean(["invertsenseGPIOPin"]) cur_senseGPIOPinPUD = self._settings.get(["senseGPIOPinPUD"]) if cur_switchingMethod == 'GPIO' or cur_sensingMethod == 'GPIO': if cur_GPIOMode == 'BOARD': # Convert BOARD pin numbers to BCM def _gpio_board_to_bcm(pin): _pin_to_gpio_rev1 = [-1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] _pin_to_gpio_rev2 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] _pin_to_gpio_rev3 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21 ] if GPIO.RPI_REVISION == 1: pin_to_gpio = _pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = _pin_to_gpio_rev2 else: pin_to_gpio = _pin_to_gpio_rev3 return pin_to_gpio[pin] try: import RPi.GPIO as GPIO _has_gpio = True except (ImportError, RuntimeError): self._logger.exception("Error importing RPi.GPIO. BOARD->BCM conversion will not occur") _has_gpio = False if cur_switchingMethod == 'GPIO' and _has_gpio: p = _gpio_board_to_bcm(cur_onoffGPIOPin) self._logger.info("Converting pin number from BOARD to BCM. onoffGPIOPin={} -> onoffGPIOPin={}".format(cur_onoffGPIOPin, p)) self._settings.set_int(["onoffGPIOPin"], p) if cur_sensingMethod == 'GPIO' and _has_gpio: p = _gpio_board_to_bcm(cur_senseGPIOPin) self._logger.info("Converting pin number from BOARD to BCM. senseGPIOPin={} -> senseGPIOPin={}".format(cur_senseGPIOPin, p)) self._settings.set_int(["senseGPIOPin"], p) if len(self._availableGPIODevices) > 0: # This was likely a Raspberry Pi using RPi.GPIO. Set GPIODevice to the first dev found which is likely /dev/gpiochip0 self._logger.info("Setting GPIODevice to the first found. GPIODevice={}".format(self._availableGPIODevices[0])) self._settings.set(["GPIODevice"], self._availableGPIODevices[0]) else: # GPIO was used for either but no GPIO devices exist. Reset to defaults. self._logger.warning("No GPIO devices found. Reverting switchingMethod and sensingMethod to defaults.") self._settings.remove(["switchingMethod"]) self._settings.remove(["sensingMethod"]) # Write the config to psucontrol_rpigpio just in case the user decides/needs to switch to it. self._logger.info("Writing original GPIO related settings to psucontrol_rpigpio.") self._settings.global_set(['plugins', 'psucontrol_rpigpio', 'GPIOMode'], cur_GPIOMode) self._settings.global_set(['plugins', 'psucontrol_rpigpio', 'switchingMethod'], cur_switchingMethod) self._settings.global_set(['plugins', 'psucontrol_rpigpio', 'sensingMethod'], cur_sensingMethod) self._settings.global_set_int(['plugins', 'psucontrol_rpigpio', 'onoffGPIOPin'], cur_onoffGPIOPin) self._settings.global_set_boolean(['plugins', 'psucontrol_rpigpio', 'invertonoffGPIOPin'], cur_invertonoffGPIOPin) self._settings.global_set_int(['plugins', 'psucontrol_rpigpio', 'senseGPIOPin'], cur_senseGPIOPin) self._settings.global_set_boolean(['plugins', 'psucontrol_rpigpio', 'invertsenseGPIOPin'], cur_invertsenseGPIOPin) self._settings.global_set(['plugins', 'psucontrol_rpigpio', 'senseGPIOPinPUD'], cur_senseGPIOPinPUD) else: self._logger.info("No GPIO pins to convert.") # Remove now unused config option self._logger.info("Removing Setting: GPIOMode") self._settings.remove(["GPIOMode"]) def get_template_vars(self): available_plugins = [] for k in list(self._sub_plugins.keys()): available_plugins.append(dict(pluginIdentifier=k, displayName=self._plugin_manager.plugins[k].name)) return { "availableGPIODevices": self._availableGPIODevices, "availablePlugins": available_plugins, "hasGPIO": HAS_GPIO, "supportsLineBias": SUPPORTS_LINE_BIAS } def get_template_configs(self): return [ dict(type="settings", custom_bindings=True) ] def get_assets(self): return { "js": ["js/psucontrol.js"], "less": ["less/psucontrol.less"], "css": ["css/psucontrol.min.css"] } def get_update_information(self): return dict( psucontrol=dict( displayName="PSU Control", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-PSUControl", current=self._plugin_version, # update method: pip w/ dependency links pip="https://github.com/kantlivelong/OctoPrint-PSUControl/archive/{target_version}.zip" ) ) def register_custom_events(self): return ["psu_state_changed"] def get_additional_permissions(self, *args, **kwargs): return [ dict(key="CONTROL", name="Control", description=gettext("Allows switching PSU on/off"), roles=["admin"], dangerous=True, default_groups=[Permissions.ADMIN_GROUP]) ] def _hook_octoprint_server_api_before_request(self, *args, **kwargs): return [self.turn_on_before_printing_after_upload]
class tasmotaPlugin( octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.ProgressPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.tasmota") self._tasmota_logger = logging.getLogger( "octoprint.plugins.tasmota.debug") self.thermal_runaway_triggered = False self.poll_status = None self.abortTimeout = 0 self._timeout_value = None self._abort_timer = None self._countdown_active = False self._waitForHeaters = False self._waitForTimelapse = False self._timelapse_active = False self._skipIdleTimer = False self.powerOffWhenIdle = False self._idleTimer = None ##~~ StartupPlugin mixin def on_startup(self, host, port): # setup customized logger from octoprint.logging.handlers import CleaningTimedRotatingFileHandler tasmota_logging_handler = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(postfix="debug"), when="D", backupCount=3) tasmota_logging_handler.setFormatter( logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) tasmota_logging_handler.setLevel(logging.DEBUG) self._tasmota_logger.addHandler(tasmota_logging_handler) self._tasmota_logger.setLevel( logging.DEBUG if self._settings.get_boolean(["debug_logging"] ) else logging.INFO) self._tasmota_logger.propagate = False self.energy_db_path = os.path.join(self.get_plugin_data_folder(), "energy_data.db") if not os.path.exists(self.energy_db_path): db = sqlite3.connect(self.energy_db_path) cursor = db.cursor() cursor.execute( '''CREATE TABLE energy_data(id INTEGER PRIMARY KEY, ip TEXT, idx TEXT, timestamp TEXT, current REAL, power REAL, total REAL, voltage REAL)''' ) db.commit() db.close() self.sensor_db_path = os.path.join(self.get_plugin_data_folder(), "sensor_data.db") if not os.path.exists(self.sensor_db_path): db = sqlite3.connect(self.sensor_db_path) cursor = db.cursor() cursor.execute( '''CREATE TABLE sensor_data(id INTEGER PRIMARY KEY, ip TEXT, idx TEXT, timestamp TEXT, temperature REAL, humidity REAL)''' ) db.commit() db.close() self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._tasmota_logger.debug("abortTimeout: %s" % self.abortTimeout) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._tasmota_logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._tasmota_logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._tasmota_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._tasmota_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) self._start_idle_timer() def on_after_startup(self): self._logger.info("Tasmota loaded!") if self._settings.get_boolean([ "polling_enabled" ]) and self._settings.get_int(["polling_interval"]) > 0: self.poll_status = RepeatedTimer( int(self._settings.get_int(["polling_interval"])) * 60, self.check_statuses) self.poll_status.start() ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict(debug_logging=False, polling_enabled=False, polling_interval=5, thermal_runaway_monitoring=False, thermal_runaway_max_bed=120, thermal_runaway_max_extruder=300, event_on_error_monitoring=False, event_on_disconnect_monitoring=False, arrSmartplugs=[], abortTimeout=30, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) old_polling_value = self._settings.get_boolean(["pollingEnabled"]) old_polling_timer = self._settings.get(["pollingInterval"]) old_powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) old_idleTimeout = self._settings.get_int(["idleTimeout"]) old_idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) old_idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) if self.powerOffWhenIdle != old_powerOffWhenIdle: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle == True: self._tasmota_logger.debug( "Settings saved, Automatic Power Off Endabled, starting idle timer..." ) self._start_idle_timer() else: self._tasmota_logger.debug( "Settings saved, Automatic Power Off Disabled, stopping idle timer..." ) self._stop_idle_timer() new_debug_logging = self._settings.get_boolean(["debug_logging"]) new_polling_value = self._settings.get_boolean(["pollingEnabled"]) new_polling_timer = self._settings.get(["pollingInterval"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._tasmota_logger.setLevel(logging.DEBUG) else: self._tasmota_logger.setLevel(logging.INFO) if old_polling_value != new_polling_value or old_polling_timer != new_polling_timer: if self.poll_status: self.poll_status.cancel() if new_polling_value: self.poll_status = RepeatedTimer( int(self._settings.get(["pollingInterval"])) * 60, self.check_statuses) self.poll_status.start() def get_settings_version(self): return 8 def on_settings_migrate(self, target, current=None): if current is None or current < 6: # Reset plug settings to defaults. self._logger.debug("Resetting arrSmartplugs for tasmota settings.") self._settings.set(['arrSmartplugs'], self.get_settings_defaults()["arrSmartplugs"]) if current == 6: # Add new fields arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["automaticShutdownEnabled"] = False arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"], arrSmartplugs_new) if current == 7 or current == 6: # Add new fields arrSmartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["event_on_error"] = False plug["event_on_disconnect"] = False arrSmartplugs_new.append(plug) self._settings.set(["arrSmartplugs"], arrSmartplugs_new) ##~~ AssetPlugin mixin def get_assets(self): return dict(js=[ "js/jquery-ui.min.js", "js/knockout-sortable.js", "js/fontawesome-iconpicker.js", "js/ko.iconpicker.js", "js/plotly-latest.min.js", "js/knockout-bootstrap.min.js", "js/tasmota.js" ], css=[ "css/font-awesome.min.css", "css/font-awesome-v4-shims.min.css", "css/fontawesome-iconpicker.css", "css/tasmota.css" ]) ##~~ TemplatePlugin mixin def get_template_configs(self): return [ dict(type="navbar", custom_bindings=True), dict(type="settings", custom_bindings=True), dict(type="tab", custom_bindings=True), dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: show_sidebar", template="tasmota_sidebar.jinja2", template_header="tasmota_sidebar_header.jinja2", styles=["display: none"]) ] ##~~ ProgressPlugin mixin def on_print_progress(self, storage, path, progress): if self.powerOffWhenIdle == True and not (self._skipIdleTimer == True): self._tasmota_logger.debug( "Resetting idle timer during print progress (%s)..." % progress) self._waitForHeaters = False self._reset_idle_timer() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): # Error Event if event == Events.ERROR and self._settings.getBoolean( ["event_on_error_monitoring"]) == True: self._tasmota_logger.debug("powering off due to %s event." % event) for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_error"] == True: self._tasmota_logger.debug( "powering off %s:%s due to %s event." % (plug["ip"], plug["idx"], event)) self.turn_off(plug["ip"], plug["idx"]) # Disconnected Event if event == Events.DISCONNECTED and self._settings.getBoolean( ["event_on_disconnect_monitoring"]) == True: self._tasmota_logger.debug("powering off due to %s event." % event) for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_disconnect"] == True: self._tasmota_logger.debug( "powering off %s:%s due to %s event." % (plug["ip"], plug["idx"], event)) self.turn_off(plug["ip"], plug["idx"]) # Client Opened Event if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) return # Print Started Event if event == Events.PRINT_STARTED and self.powerOffWhenIdle == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._tasmota_logger.debug( "Power off aborted because starting new print.") if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle == True and event == Events.MOVIE_RENDERING: self._tasmota_logger.debug("Timelapse generation started: %s" % payload.get("movie_basename", "")) self._timelapse_active = True if self._timelapse_active and event == Events.MOVIE_DONE or event == Events.MOVIE_FAILED: self._tasmota_logger.debug( "Timelapse generation finished: %s. Return Code: %s" % (payload.get("movie_basename", ""), payload.get("returncode", "completed"))) self._timelapse_active = False ##~~ SimpleApiPlugin mixin def turn_on(self, plugip, plugidx): self._tasmota_logger.debug("Turning on %s index %s." % (plugip, plugidx)) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip, "idx", plugidx) try: if plug["use_backlog"] and int(plug["backlog_on_delay"]) > 0: webresponse = requests.get( "http://" + plug["ip"] + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=backlog%20delay%20" + str(int(plug["backlog_on_delay"]) * 10) + "%3BPower" + str(plug["idx"]) + "%20on%3B") response = dict() response["POWER%s" % plug["idx"]] = "ON" else: webresponse = requests.get( "http://" + plug["ip"] + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=Power" + str(plug["idx"]) + "%20on") response = webresponse.json() chk = response["POWER%s" % plug["idx"]] except: self._tasmota_logger.error( 'Invalid ip or unknown error connecting to %s.' % plug["ip"], exc_info=True) response = "Unknown error turning on %s index %s." % (plugip, plugidx) chk = "UNKNOWN" self._tasmota_logger.debug("Response: %s" % response) if chk.upper() == "ON": if plug["autoConnect"] and self._printer.is_closed_or_error(): self._logger.info(self._settings.global_get(['serial'])) c = threading.Timer( int(plug["autoConnectDelay"]), self._printer.connect, kwargs=dict( port=self._settings.global_get(['serial', 'port']))) c.daemon = True c.start() if plug["sysCmdOn"]: t = threading.Timer(int(plug["sysCmdOnDelay"]), os.system, args=[plug["sysRunCmdOn"]]) t.daemon = True t.start() if self.powerOffWhenIdle == True and plug[ "automaticShutdownEnabled"] == True: self._tasmota_logger.debug( "Resetting idle timer since plug %s:%s was just turned on." % (plugip, plugidx)) self._waitForHeaters = False self._reset_idle_timer() self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="on", ip=plugip, idx=plugidx)) elif chk.upper() == "OFF": self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="off", ip=plugip, idx=plugidx)) else: self._tasmota_logger.debug(response) self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="unknown", ip=plugip, idx=plugidx)) def turn_off(self, plugip, plugidx): plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip, "idx", plugidx) self._tasmota_logger.debug("Turning off %s " % plug) try: if plug["use_backlog"] and int(plug["backlog_off_delay"]) > 0: self._tasmota_logger.debug( "Using backlog commands with a delay value of %s" % str(int(plug["backlog_off_delay"]) * 10)) backlog_url = "http://" + plug["ip"] + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=backlog%20delay%20" + str( int(plug["backlog_off_delay"]) * 10) + "%3BPower" + str(plug["idx"]) + "%20off%3B" self._tasmota_logger.debug("Sending command %s" % backlog_url) webresponse = requests.get(backlog_url) response = dict() response["POWER%s" % plug["idx"]] = "OFF" if plug["sysCmdOff"]: self._tasmota_logger.debug( "Running system command: %s in %s" % (plug["sysRunCmdOff"], plug["sysCmdOffDelay"])) t = threading.Timer(int(plug["sysCmdOffDelay"]), os.system, args=[plug["sysRunCmdOff"]]) t.daemon = True t.start() if plug["autoDisconnect"]: self._tasmota_logger.debug("Disconnnecting from printer") self._printer.disconnect() time.sleep(int(plug["autoDisconnectDelay"])) if not plug["use_backlog"]: self._tasmota_logger.debug("Not using backlog commands") webresponse = requests.get( "http://" + plug["ip"] + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=Power" + str(plug["idx"]) + "%20off") response = webresponse.json() chk = response["POWER%s" % plug["idx"]] except: self._tasmota_logger.error( 'Invalid ip or unknown error connecting to %s.' % plug["ip"], exc_info=True) response = "Unknown error turning off %s index %s." % (plugip, plugidx) chk = "UNKNOWN" self._tasmota_logger.debug("Response: %s" % response) if chk.upper() == "ON": self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="on", ip=plugip, idx=plugidx)) elif chk.upper() == "OFF": self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="off", ip=plugip, idx=plugidx)) else: self._tasmota_logger.debug(response) self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="unknown", ip=plugip, idx=plugidx)) def check_statuses(self): for plug in self._settings.get(["arrSmartplugs"]): self.check_status(plug["ip"], plug["idx"]) def check_status(self, plugip, plugidx): self._tasmota_logger.debug("Checking status of %s index %s." % (plugip, plugidx)) if plugip != "": try: plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip, "idx", plugidx) self._tasmota_logger.debug(plug) webresponse = requests.get( "http://" + plugip + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=Status%200") response = webresponse.json() self._tasmota_logger.debug("%s index %s response: %s" % (plugip, plugidx, response)) #chk = response["POWER%s" % plugidx] chk = self.lookup(response, *["StatusSTS", "POWER" + plugidx]) if chk is None: chk = "UNKNOWN" energy_data = self.lookup(response, *["StatusSNS", "ENERGY"]) if energy_data is not None: today = datetime.today() c = self.lookup(response, *["StatusSNS", "ENERGY", "Current"]) p = self.lookup(response, *["StatusSNS", "ENERGY", "Power"]) t = self.lookup(response, *["StatusSNS", "ENERGY", "Total"]) v = self.lookup(response, *["StatusSNS", "ENERGY", "Voltage"]) self._tasmota_logger.debug("Energy Data: %s" % energy_data) db = sqlite3.connect(self.energy_db_path) cursor = db.cursor() cursor.execute( '''INSERT INTO energy_data(ip, idx, timestamp, current, power, total, voltage) VALUES(?,?,?,?,?,?,?)''', [plugip, plugidx, today.isoformat(' '), c, p, t, v]) db.commit() db.close() if plug["sensor_identifier"] != "": sensor_data = self.lookup( response, *["StatusSNS", plug["sensor_identifier"]]) if sensor_data is not None: today = datetime.today() t = self.lookup( response, *[ "StatusSNS", plug["sensor_identifier"], "Temperature" ]) h = self.lookup( response, *[ "StatusSNS", plug["sensor_identifier"], "Humidity" ]) self._tasmota_logger.debug("Sensor Data: %s" % sensor_data) db = sqlite3.connect(self.sensor_db_path) cursor = db.cursor() cursor.execute( '''INSERT INTO sensor_data(ip, idx, timestamp, temperature, humidity) VALUES(?,?,?,?,?)''', [plugip, plugidx, today.isoformat(' '), t, h]) db.commit() db.close() else: sensor_data = None except: self._tasmota_logger.error( 'Invalid ip or unknown error connecting to %s.' % plugip, exc_info=True) response = "unknown error with %s." % plugip chk = "UNKNOWN" self._tasmota_logger.debug("%s index %s is %s" % (plugip, plugidx, chk)) if chk.upper() == "ON": self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="on", ip=plugip, idx=plugidx, energy_data=energy_data, sensor_data=sensor_data)) elif chk.upper() == "OFF": self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="off", ip=plugip, idx=plugidx, energy_data=energy_data, sensor_data=sensor_data)) else: self._tasmota_logger.debug(response) self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="unknown", ip=plugip, idx=plugidx)) def checkSetOption26(self, plugip, username, password): webresponse = requests.get("http://" + plugip + "/cm?user="******"&password="******"&cmnd=SetOption26") response = webresponse.json() self._tasmota_logger.debug(response) return response def setSetOption26(self, plugip, username, password): webresponse = requests.get("http://" + plugip + "/cm?user="******"&password="******"&cmnd=SetOption26%20ON") response = webresponse.json() self._tasmota_logger.debug(response) return response def get_api_commands(self): return dict(turnOn=["ip", "idx"], turnOff=["ip", "idx"], checkStatus=["ip", "idx"], getEnergyData=[], checkSetOption26=["ip", "username", "password"], setSetOption26=["ip", "username", "password"], enableAutomaticShutdown=[], disableAutomaticShutdown=[], abortAutomaticShutdown=[]) def on_api_command(self, command, data): self._tasmota_logger.debug(data) if not user_permission.can(): from flask import make_response return make_response("Insufficient rights", 403) if command == 'turnOn': self.turn_on("{ip}".format(**data), "{idx}".format(**data)) elif command == 'turnOff': self.turn_off("{ip}".format(**data), "{idx}".format(**data)) elif command == 'checkStatus': self.check_status("{ip}".format(**data), "{idx}".format(**data)) elif command == 'checkSetOption26': response = self.checkSetOption26("{ip}".format(**data), "{username}".format(**data), "{password}".format(**data)) import flask return flask.jsonify(response) elif command == 'setSetOption26': response = self.setSetOption26("{ip}".format(**data), "{username}".format(**data), "{password}".format(**data)) import flask return flask.jsonify(response) elif command == 'enableAutomaticShutdown': self.powerOffWhenIdle = True elif command == 'disableAutomaticShutdown': self.powerOffWhenIdle = False elif command == 'abortAutomaticShutdown': if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None for plug in self._settings.get(["arrSmartplugs"]): if plug["use_backlog"] and int(plug["backlog_off_delay"]) > 0: backlog_url = "http://" + plug["ip"] + "/cm?user="******"username"] + "&password="******"password"]) + "&cmnd=backlog" webresponse = requests.get(backlog_url) self._tasmota_logger.debug( "Cleared countdown rules for %s" % plug["ip"]) self._tasmota_logger.debug(webresponse) self._tasmota_logger.debug("Power off aborted.") self._tasmota_logger.debug("Restarting idle timer.") self._reset_idle_timer() elif command == 'getEnergyData': self._logger.info(data) response = {} if "start_date" in data and data["start_date"] != "": start_date = data["start_date"] else: start_date = datetime.date.today() - timedelta(days=1) if "end_date" in data and data["end_date"] != "": end_date = data["end_date"] else: end_date = datetime.date.today() + timedelta(days=1) energy_db = sqlite3.connect(self.energy_db_path) energy_cursor = energy_db.cursor() energy_cursor.execute( '''SELECT ip || ':' || idx AS ip, group_concat(timestamp) as timestamp, group_concat(current) as current, group_concat(power) as power, group_concat(total) as total FROM energy_data WHERE timestamp BETWEEN ? AND ? GROUP BY ip, idx''', [start_date, end_date]) response["energy_data"] = energy_cursor.fetchall() energy_db.close() sensor_db = sqlite3.connect(self.sensor_db_path) sensor_cursor = sensor_db.cursor() sensor_cursor.execute( '''SELECT ip || ':' || idx AS ip, group_concat(timestamp) as timestamp, group_concat(temperature) as temperature, group_concat(humidity) as humidity FROM sensor_data WHERE timestamp BETWEEN ? AND ? GROUP BY ip, idx''', [start_date, end_date]) response["sensor_data"] = sensor_cursor.fetchall() sensor_db.close() import flask return flask.jsonify(response) if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown": self._tasmota_logger.debug( "Automatic power off setting changed: %s" % self.powerOffWhenIdle) self._settings.set_boolean(["powerOffWhenIdle"], self.powerOffWhenIdle) self._settings.save() #eventManager().fire(Events.SETTINGS_UPDATED) if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown" or command == "abortAutomaticShutdown": self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) ##~~ Gcode processing hook def gcode_off(self, plug): self._tasmota_logger.debug("Sending gcode off") if plug["warnPrinting"] and self._printer.is_printing(): self._tasmota_logger.info( "Not powering off %s because printer is printing." % plug["label"]) else: self._tasmota_logger.debug("Sending turn off for %s index %s" % (plug["ip"], plug["idx"])) self.turn_off(plug["ip"], plug["idx"]) def gcode_on(self, plug): self.turn_on(plug["ip"], plug["idx"]) def processGCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if gcode: if gcode in ["M80", "M81"] and cmd.count(" ") >= 2: plugip = cmd.split()[1] plugidx = cmd.split()[2] for plug in self._settings.get(["arrSmartplugs"]): if plug["ip"].upper() == plugip.upper( ) and plug["idx"] == plugidx and plug["gcodeEnabled"]: if cmd.startswith("M80"): self._tasmota_logger.debug( "Received M80 command, attempting power on of %s index %s." % (plugip, plugidx)) t = threading.Timer(int(plug["gcodeOnDelay"]), self.gcode_on, [plug]) t.daemon = True t.start() return elif cmd.startswith("M81"): self._tasmota_logger.debug( "Received M81 command, attempting power off of %s index %s." % (plugip, plugidx)) t = threading.Timer(int(plug["gcodeOffDelay"]), self.gcode_off, [plug]) t.daemon = True t.start() return else: return elif self.powerOffWhenIdle and not ( gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() return ##~~ Temperatures received hook def check_temps(self, parsed_temps): for k, v in parsed_temps.items(): if k == "B" and v[0] > int( self._settings.get(["thermal_runaway_max_bed"])): self._tasmota_logger.debug( "Max bed temp reached, shutting off plugs.") self.thermal_runaway_triggered = True if k.startswith("T") and v[0] > int( self._settings.get(["thermal_runaway_max_extruder"])): self._tasmota_logger.debug( "Extruder max temp reached, shutting off plugs.") self.thermal_runaway_triggered = True if self.thermal_runaway_triggered == True: for plug in self._settings.get(['arrSmartplugs']): if plug["thermal_runaway"] == True: self.turn_off(plug["ip"], plug["idx"]) def monitor_temperatures(self, comm, parsed_temps): if self._settings.get(["thermal_runaway_monitoring" ]) and self.thermal_runaway_triggered == False: # Run inside it's own thread to prevent communication blocking t = threading.Timer(0, self.check_temps, [parsed_temps]) t.daemon = True t.start() return parsed_temps ##~~ Idle Timeout def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.daemon = True self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._waitForTimelapse: return if self._printer.is_printing() or self._printer.is_paused(): return self._tasmota_logger.debug( "Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._tasmota_logger.debug("Heaters below temperature.") if self._wait_for_timelapse(): self._timer_start() else: self._tasmota_logger.debug("Aborted power off due to activity.") ##~~ Timelapse Monitoring def _wait_for_timelapse(self): self._waitForTimelapse = True self._tasmota_logger.debug( "Checking timelapse status before shutting off power...") while True: if not self._waitForTimelapse: return False if not self._timelapse_active: self._waitForTimelapse = False return True self._tasmota_logger.debug( "Waiting for timelapse before shutting off power...") time.sleep(5) ##~~ Temperature Cooldown def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._tasmota_logger.debug("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._tasmota_logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._tasmota_logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._tasmota_logger.debug( "Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer def _timer_start(self): if self._abort_timer is not None: return self._tasmota_logger.debug("Starting abort power off timer.") self._timeout_value = self.abortTimeout self._abort_timer = RepeatedTimer(1, self._timer_task) self._abort_timer.start() def _timer_task(self): if self._timeout_value is None: return self._timeout_value -= 1 self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): self._tasmota_logger.debug("Automatically powering off enabled plugs.") for plug in self._settings.get(['arrSmartplugs']): if plug.get("automaticShutdownEnabled", False): self.turn_off("{ip}".format(**plug), "{idx}".format(**plug)) ##~~ Utility functions def lookup(self, dic, key, *keys): if keys: return self.lookup(dic.get(key, {}), *keys) return dic.get(key) def plug_search(self, list, key1, value1, key2, value2): for item in list: if item[key1] == value1 and item[key2] == value2: return item ##~~ Softwareupdate hook def get_update_information(self): # Define the configuration for your plugin to use with the Software Update # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update # for details. return dict(tasmota=dict( displayName="OctoPrint-Tasmota", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Tasmota", current=self._plugin_version, # update method: pip pip= "https://github.com/jneilliii/OctoPrint-Tasmota/archive/{target_version}.zip" ))
class NeoPSUControl(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): try: # global GPIO # import RPi.GPIO as GPIO self._hasGPIO = True except (ImportError, RuntimeError): self._hasGPIO = False self._pin_to_gpio = Maps() # self._pin_to_gpio_rev1 = [-1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] # self._pin_to_gpio_rev2 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] # self._pin_to_gpio_rev3 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21 ] self.GPIOMode = '' self.switchingMethod = '' self.onoffGPIOPin = 0 self.invertonoffGPIOPin = False self.onGCodeCommand = '' self.offGCodeCommand = '' self.onSysCommand = '' self.offSysCommand = '' self.enablePseudoOnOff = False self.pseudoOnGCodeCommand = '' self.pseudoOffGCodeCommand = '' self.postOnDelay = 0.0 self.autoOn = False self.autoOnTriggerGCodeCommands = '' self._autoOnTriggerGCodeCommandsArray = [] self.enablePowerOffWarningDialog = True self.powerOffWhenIdle = False self.idleTimeout = 0 self.idleIgnoreCommands = '' self._idleIgnoreCommandsArray = [] self.idleTimeoutWaitTemp = 0 self.disconnectOnPowerOff = False self.sensingMethod = '' self.sensePollingInterval = 0 self.senseGPIOPin = 0 self.invertsenseGPIOPin = False self.senseGPIOPinPUD = '' self.senseSystemCommand = '' self.isPSUOn = False self._noSensing_isPSUOn = False self._check_psu_state_thread = None self._check_psu_state_event = threading.Event() self._idleTimer = None self._waitForHeaters = False self._skipIdleTimer = False self._configuredGPIOPins = [] def on_settings_initialized(self): self.GPIOMode = self._settings.get(["GPIOMode"]) self._logger.debug("GPIOMode: %s" % self.GPIOMode) self.switchingMethod = self._settings.get(["switchingMethod"]) self._logger.debug("switchingMethod: %s" % self.switchingMethod) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self._logger.debug("onoffGPIOPin: %s" % self.onoffGPIOPin) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self._logger.debug("invertonoffGPIOPin: %s" % self.invertonoffGPIOPin) self.onGCodeCommand = self._settings.get(["onGCodeCommand"]) self._logger.debug("onGCodeCommand: %s" % self.onGCodeCommand) self.offGCodeCommand = self._settings.get(["offGCodeCommand"]) self._logger.debug("offGCodeCommand: %s" % self.offGCodeCommand) self.onSysCommand = self._settings.get(["onSysCommand"]) self._logger.debug("onSysCommand: %s" % self.onSysCommand) 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.connectOnPowerOn = self._settings.get_boolean( ["connectOnPowerOn"]) self._logger.debug("connectOnPowerOn: %s" % self.connectOnPowerOn) 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.sensePollingInterval = self._settings.get_int( ["sensePollingInterval"]) self._logger.debug("sensePollingInterval: %s" % self.sensePollingInterval) 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) self.autoOnTriggerGCodeCommands = self._settings.get( ["autoOnTriggerGCodeCommands"]) self._autoOnTriggerGCodeCommandsArray = self.autoOnTriggerGCodeCommands.split( ',') self._logger.debug("autoOnTriggerGCodeCommands: %s" % self.autoOnTriggerGCodeCommands) self.enablePowerOffWarningDialog = self._settings.get_boolean( ["enablePowerOffWarningDialog"]) self._logger.debug("enablePowerOffWarningDialog: %s" % self.enablePowerOffWarningDialog) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) scripts = self._settings.listScripts("gcode") if not "neopsucontrol_post_on" in scripts: self._settings.saveScript("gcode", "neopsucontrol_post_on", u'') scripts = self._settings.listScripts("gcode") if not "neopsucontrol_pre_off" in scripts: self._settings.saveScript("gcode", "neopsucontrol_pre_off", u'') if self.switchingMethod == 'GCODE': self._logger.info("Using G-Code Commands for On/Off") elif self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") elif self.switchingMethod == 'SYSTEM': self._logger.info("Using System Commands for On/Off") if self.sensingMethod == 'INTERNAL': self._logger.info("Using internal tracking for PSU on/off state.") elif self.sensingMethod == 'GPIO': self._logger.info("Using GPIO for tracking PSU on/off state.") elif self.sensingMethod == 'SYSTEM': self._logger.info( "Using System Commands for tracking PSU on/off state.") if self.switchingMethod == 'GPIO' or self.sensingMethod == 'GPIO': self._configure_gpio() self._check_psu_state_thread = threading.Thread( target=self._check_psu_state) self._check_psu_state_thread.daemon = True self._check_psu_state_thread.start() self._start_idle_timer() def _gpio_board_to_bcm(self, pin): return pin # self._pin_to_gpio.gpios[pin] #if GPIO.RPI_REVISION == 1: # pin_to_gpio = self._pin_to_gpio_rev1 #elif GPIO.RPI_REVISION == 2: # pin_to_gpio = self._pin_to_gpio_rev2 #else: # pin_to_gpio = self._pin_to_gpio_rev3 #return pin_to_gpio[pin] def _gpio_bcm_to_board(self, pin): #if GPIO.RPI_REVISION == 1: # pin_to_gpio = self._pin_to_gpio_rev1 #elif GPIO.RPI_REVISION == 2: # pin_to_gpio = self._pin_to_gpio_rev2 #else: # pin_to_gpio = self._pin_to_gpio_rev3 return pin # self._pin_to_gpio.gpios.index(pin) # return pin_to_gpio.index(pin) def _gpio_get_pin(self, pin): if (GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BOARD') or (GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BCM'): return pin elif GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BCM': return self._gpio_bcm_to_board(pin) elif GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BOARD': return self._gpio_board_to_bcm(pin) else: return 0 def _configure_gpio(self): if not self._hasGPIO: self._logger.error( "If you're seeing this then you must not be using a Udoo Neo") return #if GPIO.getmode() is None: # if self.GPIOMode == 'BOARD': # GPIO.setmode(GPIO.BOARD) # elif self.GPIOMode == 'BCM': # GPIO.setmode(GPIO.BCM) # else: # return self._gpios = Gpio() 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) try: self._configuredGPIOPins.append(self.senseGPIOPin) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") self._logger.info("Configuring GPIO for pin %s" % self.onoffGPIOPin) try: if not self.invertonoffGPIOPin: initial_pin_output = self._gpios.LOW else: initial_pin_output = self._gpios.HIGH self._gpios.pinMode(self.onoffGPIOPin, self._gpios.OUTPUT) self._gpios.digitalWrite(initial_pin_output) self._configuredGPIOPins.append(self.onoffGPIOPin) except (RuntimeError, ValueError) as e: self._logger.error(e) def check_psu_state(self): self._check_psu_state_event.set() def _check_psu_state(self): while True: old_isPSUOn = self.isPSUOn if self.sensingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Polling PSU state...") r = 0 try: r = self._gpios.digitalRead(self.senseGPIOPin, redirect=False) except (RuntimeError, ValueError) as e: self._logger.error(e) self._logger.debug("Result: %s" % r) if r == 1: new_isPSUOn = True elif r == 0: 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, shell=True) self._logger.debug( "Sensing system command executed. PID=%s, Command=%s" % (p.pid, self.senseSystemCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Sensing system command returned: %s" % r) if r == 0: new_isPSUOn = True elif r == 1: new_isPSUOn = False self.isPSUOn = new_isPSUOn elif self.sensingMethod == 'INTERNAL': self.isPSUOn = self._noSensing_isPSUOn else: return self._logger.debug("isPSUOn: %s" % self.isPSUOn) if (old_isPSUOn != self.isPSUOn) and self.isPSUOn: self._start_idle_timer() elif (old_isPSUOn != self.isPSUOn) and not self.isPSUOn: self._stop_idle_timer() self._plugin_manager.send_plugin_message( self._identifier, dict(isPSUOn=self.isPSUOn)) self._check_psu_state_event.wait(self.sensePollingInterval) self._check_psu_state_event.clear() def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle and self.isPSUOn: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._printer.is_printing() or self._printer.is_paused(): return self._logger.info( "Idle timeout reached after %s minute(s). Turning heaters off prior to shutting off PSU." % self.idleTimeout) if self._wait_for_heaters(): self._logger.info("Heaters below temperature.") self.turn_psu_off() else: self._logger.info("Aborted PSU shut down due to activity.") def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._logger.info("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._logger.info( "Waiting for heaters(%s) before shutting off PSU..." % ', '.join(heaters_above_waittemp)) 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("NeoPSUControl: ok") skipQueuing = True elif gcode == self.pseudoOffGCodeCommand: self.turn_psu_off() comm_instance._log("NwoPSUControl: 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() if self.powerOffWhenIdle and self.isPSUOn and not self._skipIdleTimer: if not (gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_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") if self.switchingMethod == 'GCODE': self._logger.debug("Switching PSU On Using GCODE: %s" % self.onGCodeCommand) self._printer.commands(self.onGCodeCommand) elif self.switchingMethod == 'SYSTEM': self._logger.debug("Switching PSU On Using SYSTEM: %s" % self.onSysCommand) p = subprocess.Popen(self.onSysCommand, shell=True) self._logger.debug( "On system command executed. PID=%s, Command=%s" % (p.pid, self.onSysCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("On system command returned: %s" % r) elif self.switchingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Switching PSU On Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = self._gpios.HIGH else: pin_output = self._gpios.LOW try: self._gpios.digitalWrite( self._gpio_get_pin(self.onoffGPIOPin), pin_output) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.sensingMethod not in ('GPIO', 'SYSTEM'): self._noSensing_isPSUOn = True time.sleep(0.1 + self.postOnDelay) self.check_psu_state() if self.connectOnPowerOn and self._printer.is_closed_or_error(): self._printer.connect() time.sleep(0.1) if not self._printer.is_closed_or_error(): self._printer.script("neopsucontrol_post_on", must_be_set=False) def turn_psu_off(self): if self.switchingMethod == 'GCODE' or self.switchingMethod == 'GPIO' or self.switchingMethod == 'SYSTEM': if not self._printer.is_closed_or_error(): self._printer.script("neopsucontrol_pre_off", must_be_set=False) self._logger.info("Switching PSU Off") if self.switchingMethod == 'GCODE': self._logger.debug("Switching PSU Off Using GCODE: %s" % self.offGCodeCommand) self._printer.commands(self.offGCodeCommand) elif self.switchingMethod == 'SYSTEM': self._logger.debug("Switching PSU Off Using SYSTEM: %s" % self.offSysCommand) p = subprocess.Popen(self.offSysCommand, shell=True) self._logger.debug( "Off system command executed. PID=%s, Command=%s" % (p.pid, self.offSysCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Off system command returned: %s" % r) elif self.switchingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Switching PSU Off Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = self._gpios.LOW else: pin_output = self._gpios.HIGH try: # GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) self._gpios.digitalWrite(self.onoffGPIOPin, pin_output) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.disconnectOnPowerOff: self._printer.disconnect() if self.sensingMethod not in ('GPIO', 'SYSTEM'): self._noSensing_isPSUOn = False time.sleep(0.1) self.check_psu_state() def on_event(self, event, payload): if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message( self._identifier, dict(hasGPIO=self._hasGPIO, isPSUOn=self.isPSUOn)) return def get_api_commands(self): return dict(turnPSUOn=[], turnPSUOff=[], togglePSU=[], getPSUState=[]) def on_api_get(self, request): return self.on_api_command("getPSUState", []) def on_api_command(self, command, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if command == 'turnPSUOn': 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(GPIOMode='BOARD', switchingMethod='GCODE', onoffGPIOPin=0, invertonoffGPIOPin=False, onGCodeCommand='M80', offGCodeCommand='M81', onSysCommand='', offSysCommand='', enablePseudoOnOff=False, pseudoOnGCodeCommand='M80', pseudoOffGCodeCommand='M81', postOnDelay=0.0, connectOnPowerOn=False, disconnectOnPowerOff=False, sensingMethod='INTERNAL', senseGPIOPin=0, sensePollingInterval=5, invertsenseGPIOPin=False, senseGPIOPinPUD='', senseSystemCommand='', autoOn=False, autoOnTriggerGCodeCommands= "G0,G1,G2,G3,G10,G11,G28,G29,G32,M104,M106,M109,M140,M190", enablePowerOffWarningDialog=True, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50) def on_settings_save(self, data): old_GPIOMode = self.GPIOMode old_onoffGPIOPin = self.onoffGPIOPin 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"]) self.switchingMethod = self._settings.get(["switchingMethod"]) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self.invertonoffGPIOPin = self._settings.get_boolean( ["invertonoffGPIOPin"]) self.onGCodeCommand = self._settings.get(["onGCodeCommand"]) 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.connectOnPowerOn = self._settings.get_boolean( ["connectOnPowerOn"]) self.disconnectOnPowerOff = self._settings.get_boolean( ["disconnectOnPowerOff"]) self.sensingMethod = self._settings.get(["sensingMethod"]) self.senseGPIOPin = self._settings.get_int(["senseGPIOPin"]) self.sensePollingInterval = self._settings.get_int( ["sensePollingInterval"]) 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( ',') self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self.enablePowerOffWarningDialog = self._settings.get_boolean( ["enablePowerOffWarningDialog"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) if 'scripts_gcode_psucontrol_post_on' in data: script = data["scripts_gcode_psucontrol_post_on"] self._settings.saveScript( "gcode", "psucontrol_post_on", u'' + script.replace("\r\n", "\n").replace("\r", "\n")) if 'scripts_gcode_psucontrol_pre_off' in data: script = data["scripts_gcode_psucontrol_pre_off"] self._settings.saveScript( "gcode", "psucontrol_pre_off", u'' + script.replace("\r\n", "\n").replace("\r", "\n")) #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_sensingMethod != self.sensingMethod or old_invertsenseGPIOPin != self.invertsenseGPIOPin or old_senseGPIOPinPUD != self.senseGPIOPinPUD or old_switchingMethod != self.switchingMethod) and (self.switchingMethod == 'GPIO' or self.sensingMethod == 'GPIO')): self._configure_gpio() self._start_idle_timer() def get_settings_version(self): return 3 def on_settings_migrate(self, target, current=None): if current is None: current = 0 if current < 2: # v2 changes names of settings variables to accomidate system commands. cur_switchingMethod = self._settings.get(["switchingMethod"]) if cur_switchingMethod is not None and cur_switchingMethod == "COMMAND": self._logger.info( "Migrating Setting: switchingMethod=COMMAND -> switchingMethod=GCODE" ) self._settings.set(["switchingMethod"], "GCODE") cur_onCommand = self._settings.get(["onCommand"]) if cur_onCommand is not None: self._logger.info( "Migrating Setting: onCommand={0} -> onGCodeCommand={0}". format(cur_onCommand)) self._settings.set(["onGCodeCommand"], cur_onCommand) self._settings.remove(["onCommand"]) cur_offCommand = self._settings.get(["offCommand"]) if cur_offCommand is not None: self._logger.info( "Migrating Setting: offCommand={0} -> offGCodeCommand={0}". format(cur_offCommand)) self._settings.set(["offGCodeCommand"], cur_offCommand) self._settings.remove(["offCommand"]) cur_autoOnCommands = self._settings.get(["autoOnCommands"]) if cur_autoOnCommands is not None: self._logger.info( "Migrating Setting: autoOnCommands={0} -> autoOnTriggerGCodeCommands={0}" .format(cur_autoOnCommands)) 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=True)] def get_assets(self): return { "js": ["js/neopsucontrol.js"], "less": ["less/neopsucontrol.less"], "css": ["css/neopsucontrol.min.css"] } def get_update_information(self): return dict(neopsucontrol=dict( displayName="NEO PSU Control", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="neo-psu-control", current=self._plugin_version, # update method: pip w/ dependency links pip= "https://smerkousdavid/neo-psu-control/archive/{target_version}.zip" ))
class wemoswitchPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.wemoswitch") self._wemoswitch_logger = logging.getLogger( "octoprint.plugins.wemoswitch.debug") self.discovered_devices = [] self.abortTimeout = 0 self._timeout_value = None self._abort_timer = None self._countdown_active = False self._waitForHeaters = False self._waitForTimelapse = False self._timelapse_active = False self._skipIdleTimer = False self.powerOffWhenIdle = False self._idleTimer = None self.idleTimeout = 30 self.idleIgnoreCommands = 'M105' self._idleIgnoreCommandsArray = [] self.idleTimeoutWaitTemp = 50 ##~~ StartupPlugin mixin def on_startup(self, host, port): # setup customized logger from octoprint.logging.handlers import CleaningTimedRotatingFileHandler wemoswitch_logging_handler = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(postfix="debug"), when="D", backupCount=3) wemoswitch_logging_handler.setFormatter( logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) wemoswitch_logging_handler.setLevel(logging.DEBUG) self._wemoswitch_logger.addHandler(wemoswitch_logging_handler) self._wemoswitch_logger.setLevel( logging.DEBUG if self._settings.get_boolean(["debug_logging"] ) else logging.INFO) self._wemoswitch_logger.propagate = False def on_after_startup(self): self._logger.info("WemoSwitch loaded!") self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._wemoswitch_logger.debug("abortTimeout: %s" % self.abortTimeout) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._wemoswitch_logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._wemoswitch_logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._wemoswitch_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._wemoswitch_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) if self._settings.get_boolean(["event_on_startup_monitoring"]): for plug in self._settings.get(['arrSmartplugs']): if plug["event_on_startup"] is True: self.turn_on(plug["ip"]) self._reset_idle_timer() ##~~ SettingsPlugin mixin def get_discovered_device(self, index): tmp_ret = self.discovered_devices[index] return { "label": tmp_ret.name, "ip": "{}:{}".format(tmp_ret.host, tmp_ret.port), "sn": tmp_ret.serialnumber } def get_discovered_devices(self): self._wemoswitch_logger.debug("Discovering devices") self.discovered_devices = pywemo.discover_devices() tmp_ret = [] for index in range(len(self.discovered_devices)): d = self.get_discovered_device(index) tmp_ret.append(d) return tmp_ret def get_settings_defaults(self): return dict(debug_logging=False, arrSmartplugs=[], pollingInterval=15, pollingEnabled=False, thermal_runaway_monitoring=False, thermal_runaway_max_bed=0, thermal_runaway_max_extruder=0, abortTimeout=30, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50, event_on_upload_monitoring=False, event_on_startup_monitoring=False) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) old_power_off_when_idle = self._settings.get_boolean( ["powerOffWhenIdle"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) if self.powerOffWhenIdle != old_power_off_when_idle: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle: self._wemoswitch_logger.debug( "Settings saved, Automatic Power Off Enabled, starting idle timer..." ) self._reset_idle_timer() new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._wemoswitch_logger.setLevel(logging.DEBUG) else: self._wemoswitch_logger.setLevel(logging.INFO) def get_settings_version(self): return 3 def on_settings_migrate(self, target, current=None): if current is None or current < 1: # Reset plug settings to defaults. self._logger.debug( "Resetting arrSmartplugs for wemoswitch settings.") self._settings.set(['arrSmartplugs'], self.get_settings_defaults()["arrSmartplugs"]) if current == 1: self._logger.debug( "Adding new plug settings thermal_runaway, automaticShutdownEnabled, event_on_upload, event_on_startup." ) arr_smartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["thermal_runaway"] = False plug["automaticShutdownEnabled"] = False plug["event_on_upload"] = False plug["event_on_startup"] = False arr_smartplugs_new.append(plug) self._settings.set(["arrSmartplugs"], arr_smartplugs_new) if current == 2: self._logger.debug( "Adding new plug settings automaticShutdownEnabled, event_on_upload, event_on_startup." ) arr_smartplugs_new = [] for plug in self._settings.get(['arrSmartplugs']): plug["automaticShutdownEnabled"] = False plug["event_on_upload"] = False plug["event_on_startup"] = False arr_smartplugs_new.append(plug) self._settings.set(["arrSmartplugs"], arr_smartplugs_new) ##~~ AssetPlugin mixin def get_assets(self): return dict(js=[ "js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/fontawesome-iconpicker.js", "js/ko.iconpicker.js", "js/wemoswitch.js" ], css=[ "css/font-awesome.min.css", "css/font-awesome-v4-shims.min.css", "css/fontawesome-iconpicker.css", "css/wemoswitch.css" ]) ##~~ TemplatePlugin mixin def get_template_configs(self): return [ dict(type="navbar", custom_bindings=True), dict(type="settings", custom_bindings=True), dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: show_sidebar", template_header="wemoswitch_sidebar_header.jinja2") ] ##~~ SimpleApiPlugin mixin def turn_on(self, plugip): self._wemoswitch_logger.debug("Turning on %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip) self._wemoswitch_logger.debug(plug) chk = self.sendCommand("on", plugip) if chk == 0: self.check_status(plugip) if plug["autoConnect"]: t = threading.Timer(int(plug["autoConnectDelay"]), self._printer.connect) t.start() if plug["sysCmdOn"]: t = threading.Timer(int(plug["sysCmdOnDelay"]), os.system, args=[plug["sysRunCmdOn"]]) t.start() self._reset_idle_timer() return "on" def turn_off(self, plugip): self._wemoswitch_logger.debug("Turning off %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip) self._wemoswitch_logger.debug(plug) if plug["sysCmdOff"]: t = threading.Timer(int(plug["sysCmdOffDelay"]), os.system, args=[plug["sysRunCmdOff"]]) t.start() if plug["autoDisconnect"]: self._printer.disconnect() time.sleep(int(plug["autoDisconnectDelay"])) chk = self.sendCommand("off", plugip) if chk == 0: self.check_status(plugip) def check_status(self, plugip): self._wemoswitch_logger.debug("Checking status of %s." % plugip) if plugip != "": chk = self.sendCommand("info", plugip) if chk == 1: self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="on", ip=plugip)) elif chk == 8: self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="on", ip=plugip)) elif chk == 0: self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="off", ip=plugip)) else: self._wemoswitch_logger.debug(chk) self._plugin_manager.send_plugin_message( self._identifier, dict(currentState="unknown", ip=plugip)) def get_api_commands(self): return dict(turnOn=["ip"], turnOff=["ip"], checkStatus=["ip"], enableAutomaticShutdown=[], disableAutomaticShutdown=[], abortAutomaticShutdown=[]) def on_api_get(self, request): if not Permissions.PLUGIN_WEMOSWITCH_CONTROL.can(): return flask.make_response("Insufficient rights", 403) if request.args.get("discover_devices"): return flask.jsonify( {"discovered_devices": self.get_discovered_devices()}) def on_api_command(self, command, data): if not Permissions.PLUGIN_WEMOSWITCH_CONTROL.can(): return flask.make_response("Insufficient rights", 403) if command == 'turnOn': self.turn_on("{ip}".format(**data)) elif command == 'turnOff': self.turn_off("{ip}".format(**data)) elif command == 'checkStatus': self.check_status("{ip}".format(**data)) elif command == 'enableAutomaticShutdown': self._wemoswitch_logger.debug( "enabling automatic power off on idle") self.powerOffWhenIdle = True self._reset_idle_timer() self._settings.set_boolean(["powerOffWhenIdle"], True) self._settings.save(trigger_event=True) return flask.jsonify(dict(powerOffWhenIdle=self.powerOffWhenIdle)) elif command == 'disableAutomaticShutdown': self._wemoswitch_logger.debug( "disabling automatic power off on idle") self.powerOffWhenIdle = False self._stop_idle_timer() if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._settings.set_boolean(["powerOffWhenIdle"], False) self._settings.save(trigger_event=True) return flask.jsonify(dict(powerOffWhenIdle=self.powerOffWhenIdle)) elif command == 'abortAutomaticShutdown': if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._wemoswitch_logger.debug("Power off aborted.") self._wemoswitch_logger.debug("Restarting idle timer.") self._reset_idle_timer() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): # Client Opened Event if event == Events.CLIENT_OPENED: if self._settings.get_boolean(["powerOffWhenIdle"]): self._reset_idle_timer() self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) return # Print Started Event if event == Events.PRINT_STARTED and self.powerOffWhenIdle is True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._tplinksmartplug_logger.debug( "Power off aborted because starting new print.") if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) # Cancelled Print Interpreted Event if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error( ) and self.powerOffWhenIdle is True: self._reset_idle_timer() # Print Done Event if event == Events.PRINT_DONE and self.powerOffWhenIdle is True: self._reset_idle_timer() # Timelapse Events if self.powerOffWhenIdle is True and event == Events.MOVIE_RENDERING: self._wemoswitch_logger.debug("Timelapse generation started: %s" % payload.get("movie_basename", "")) self._timelapse_active = True if self._timelapse_active and event == Events.MOVIE_DONE or event == Events.MOVIE_FAILED: self._wemoswitch_logger.debug( "Timelapse generation finished: %s. Return Code: %s" % (payload.get("movie_basename", ""), payload.get("returncode", "completed"))) self._timelapse_active = False # File Uploaded Event if event == Events.UPLOAD and self._settings.getBoolean( ["event_on_upload_monitoring"]): if payload.get("print", False): # implemented in OctoPrint version 1.4.1 self._wemoswitch_logger.debug( "File uploaded: %s. Turning enabled plugs on." % payload.get("name", "")) self._wemoswitch_logger.debug(payload) for plug in self._settings.get(['arrSmartplugs']): self._wemoswitch_logger.debug(plug) if plug["event_on_upload"] is True and not self._printer.is_ready( ): self._wemoswitch_logger.debug( "powering on %s due to %s event." % (plug["ip"], event)) response = self.turn_on(plug["ip"]) if response == "on": self._wemoswitch_logger.debug( "power on successful for %s attempting connection in %s seconds" % (plug["ip"], plug.get( "autoConnectDelay", "0"))) if payload.get("path", False) is not False and payload.get( "target") == "local": time.sleep( int(plug.get("autoConnectDelay", "0")) + 1) if self._printer.is_ready(): self._wemoswitch_logger.debug( "printer connected starting print of %s" % (payload.get("path", ""))) self._printer.select_file( payload.get("path"), False, printAfterSelect=True) ##~~ Idle Timeout def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._waitForTimelapse: return if self._printer.is_printing() or self._printer.is_paused(): return self._wemoswitch_logger.debug( "Idle timeout reached after %s minute(s). Waiting for hot end to cool prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._wemoswitch_logger.debug("Heaters below temperature.") if self._wait_for_timelapse(): self._timer_start() else: self._wemoswitch_logger.debug("Aborted power off due to activity.") ##~~ Timelapse Monitoring def _wait_for_timelapse(self): self._waitForTimelapse = True self._wemoswitch_logger.debug( "Checking timelapse status before shutting off power...") while True: if not self._waitForTimelapse: return False if not self._timelapse_active: self._waitForTimelapse = False return True self._wemoswitch_logger.debug( "Waiting for timelapse before shutting off power...") time.sleep(5) ##~~ Temperature Cooldown def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._wemoswitch_logger.debug("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._wemoswitch_logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._wemoswitch_logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._wemoswitch_logger.debug( "Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer def _timer_start(self): if self._abort_timer is not None: return self._wemoswitch_logger.debug("Starting abort power off timer.") self._timeout_value = self.abortTimeout self._abort_timer = RepeatedTimer(1, self._timer_task) self._abort_timer.start() def _timer_task(self): if self._timeout_value is None: return self._timeout_value -= 1 self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): self._wemoswitch_logger.debug( "Automatically powering off enabled plugs.") for plug in self._settings.get(['arrSmartplugs']): if plug.get("automaticShutdownEnabled", False): self.turn_off("{ip}".format(**plug)) ##~~ Utilities def plug_search(self, list, key, value): for item in list: if item[key] == value: return item def sendCommand(self, cmd, plugip): # try to connect via ip address port = None try: if ':' in plugip: plugip, port = plugip.split(':', 1) port = int(port) socket.inet_aton(plugip) self._wemoswitch_logger.debug("IP %s is valid." % plugip) except socket.error or ValueError: # try to convert hostname to ip self._wemoswitch_logger.debug("Invalid ip %s trying hostname." % plugip) try: plugip = socket.gethostbyname(plugip) self._wemoswitch_logger.debug("Hostname %s is valid." % plugip) except (socket.herror, socket.gaierror): self._wemoswitch_logger.debug("Invalid hostname %s." % plugip) return 3 try: self._wemoswitch_logger.debug("Attempting to connect to %s" % plugip) if port is None: port = pywemo.ouimeaux_device.probe_wemo(plugip) url = 'http://%s:%s/setup.xml' % (plugip, port) url = url.replace(':None', '') self._wemoswitch_logger.debug("Getting device info from %s" % url) device = pywemo.discovery.device_from_description(url, None) self._wemoswitch_logger.debug("Found device %s" % device) self._wemoswitch_logger.debug("Sending command %s to %s" % (cmd, plugip)) if cmd == "info": return device.get_state() elif cmd == "on": device.on() return 0 elif cmd == "off": device.off() return 0 except socket.error: self._wemoswitch_logger.debug("Could not connect to %s." % plugip) return 3 ##~~ Access Permissions Hook def get_additional_permissions(self, *args, **kwargs): return [ dict(key="CONTROL", name="Control Plugs", description=gettext("Allows control of configured plugs."), roles=["admin"], dangerous=True, default_groups=[ADMIN_GROUP]) ] ##~~ Gcode processing hook def gcode_turn_off(self, plug): if plug["warnPrinting"] and self._printer.is_printing(): self._logger.info( "Not powering off %s because printer is printing." % plug["label"]) else: self.turn_off(plug["ip"]) def processAtCommand(self, comm_instance, phase, command, parameters, tags=None, *args, **kwargs): if command in ["WEMOON", "WEMOOFF"]: plugip = parameters.strip() self._wemoswitch_logger.debug( "Received %s command, attempting power on of %s." % (command, plugip)) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip) self._wemoswitch_logger.debug(plug) else: return None if command == "WEMOON": if plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOnDelay"]), self.turn_on, args=[plugip]) t.start() return None if command == "WEMOOFF": if plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOffDelay"]), self.gcode_turn_off, args=[plug]) t.start() return None def processGCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if self.powerOffWhenIdle and not (gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() if gcode: if cmd.startswith("M80"): plugip = re.sub(r'^M80\s?', '', cmd) self._wemoswitch_logger.debug( "Received M80 command, attempting power on of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip) self._wemoswitch_logger.debug(plug) if plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOnDelay"]), self.turn_on, args=[plugip]) t.start() return elif cmd.startswith("M81"): plugip = re.sub(r'^M81\s?', '', cmd) self._wemoswitch_logger.debug( "Received M81 command, attempting power off of %s." % plugip) plug = self.plug_search(self._settings.get(["arrSmartplugs"]), "ip", plugip) self._wemoswitch_logger.debug(plug) if plug["gcodeEnabled"]: t = threading.Timer(int(plug["gcodeOffDelay"]), self.gcode_turn_off, [plug]) t.start() return else: return def check_temps(self, parsed_temps): thermal_runaway_triggered = False for k, v in parsed_temps.items(): if k == "B" and v[1] > 0 and v[0] > int( self._settings.get(["thermal_runaway_max_bed"])): self._wemoswitch_logger.debug( "Max bed temp reached, shutting off plugs.") thermal_runaway_triggered = True if k.startswith("T") and v[1] > 0 and v[0] > int( self._settings.get(["thermal_runaway_max_extruder"])): self._wemoswitch_logger.debug( "Extruder max temp reached, shutting off plugs.") thermal_runaway_triggered = True if thermal_runaway_triggered == True: for plug in self._settings.get(['arrSmartplugs']): if plug["thermal_runaway"] == True: response = self.turn_off(plug["ip"]) if response["currentState"] == "off": self._plugin_manager.send_plugin_message( self._identifier, response) def monitor_temperatures(self, comm, parsed_temps): if self._settings.get(["thermal_runaway_monitoring"]): # Run inside it's own thread to prevent communication blocking t = threading.Timer(0, self.check_temps, [parsed_temps]) t.start() return parsed_temps ##~~ Softwareupdate hook def get_update_information(self): return dict(wemoswitch=dict( displayName="Wemo Switch", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-WemoSwitch", current=self._plugin_version, stable_branch=dict( name="Stable", branch="master", comittish=["master"]), prerelease_branches=[ dict( name="Release Candidate", branch="rc", comittish=["rc", "master"], ) ], # update method: pip pip= "https://github.com/jneilliii/OctoPrint-WemoSwitch/archive/{target_version}.zip" ))
class PSUControl(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): try: global GPIO import RPi.GPIO as GPIO self._hasGPIO = True except (ImportError, RuntimeError): self._hasGPIO = False self._pin_to_gpio_rev1 = [-1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev2 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] self._pin_to_gpio_rev3 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21 ] self.GPIOMode = '' self.switchingMethod = '' self.onoffGPIOPin = 0 self.invertonoffGPIOPin = False self.onGCodeCommand = '' self.offGCodeCommand = '' self.onSysCommand = '' self.offSysCommand = '' self.enablePseudoOnOff = False self.pseudoOnGCodeCommand = '' self.pseudoOffGCodeCommand = '' self.postOnDelay = 0.0 self.autoOn = False self.autoOnTriggerGCodeCommands = '' self._autoOnTriggerGCodeCommandsArray = [] self.enablePowerOffWarningDialog = True self.powerOffWhenIdle = False self.idleTimeout = 0 self.idleIgnoreCommands = '' self._idleIgnoreCommandsArray = [] self.idleTimeoutWaitTemp = 0 self.disconnectOnPowerOff = False self.sensingMethod = '' self.senseGPIOPin = 0 self.invertsenseGPIOPin = False self.senseGPIOPinPUD = '' self.senseSystemCommand = '' self.isPSUOn = False self._noSensing_isPSUOn = False self._check_psu_state_thread = None self._check_psu_state_event= threading.Event() self._idleTimer = None self._waitForHeaters = False self._skipIdleTimer = False self._configuredGPIOPins = [] def on_settings_initialized(self): self.GPIOMode = self._settings.get(["GPIOMode"]) self._logger.debug("GPIOMode: %s" % self.GPIOMode) self.switchingMethod = self._settings.get(["switchingMethod"]) self._logger.debug("switchingMethod: %s" % self.switchingMethod) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self._logger.debug("onoffGPIOPin: %s" % self.onoffGPIOPin) self.invertonoffGPIOPin = self._settings.get_boolean(["invertonoffGPIOPin"]) self._logger.debug("invertonoffGPIOPin: %s" % self.invertonoffGPIOPin) self.onGCodeCommand = self._settings.get(["onGCodeCommand"]) self._logger.debug("onGCodeCommand: %s" % self.onGCodeCommand) self.offGCodeCommand = self._settings.get(["offGCodeCommand"]) self._logger.debug("offGCodeCommand: %s" % self.offGCodeCommand) self.onSysCommand = self._settings.get(["onSysCommand"]) self._logger.debug("onSysCommand: %s" % self.onSysCommand) 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.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) self.autoOnTriggerGCodeCommands = self._settings.get(["autoOnTriggerGCodeCommands"]) self._autoOnTriggerGCodeCommandsArray = self.autoOnTriggerGCodeCommands.split(',') self._logger.debug("autoOnTriggerGCodeCommands: %s" % self.autoOnTriggerGCodeCommands) self.enablePowerOffWarningDialog = self._settings.get_boolean(["enablePowerOffWarningDialog"]) self._logger.debug("enablePowerOffWarningDialog: %s" % self.enablePowerOffWarningDialog) self.powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) self._logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int(["idleTimeoutWaitTemp"]) self._logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) if self.switchingMethod == 'GCODE': self._logger.info("Using G-Code Commands for On/Off") elif self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") elif self.switchingMethod == 'SYSTEM': self._logger.info("Using System Commands for On/Off") if self.sensingMethod == 'INTERNAL': self._logger.info("Using internal tracking for PSU on/off state.") elif self.sensingMethod == 'GPIO': self._logger.info("Using GPIO for tracking PSU on/off state.") elif self.sensingMethod == 'SYSTEM': self._logger.info("Using System Commands for tracking PSU on/off state.") if self.switchingMethod == 'GPIO' or self.sensingMethod == 'GPIO': self._configure_gpio() self._check_psu_state_thread = threading.Thread(target=self._check_psu_state) self._check_psu_state_thread.daemon = True self._check_psu_state_thread.start() self._start_idle_timer() def _gpio_board_to_bcm(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio[pin] def _gpio_bcm_to_board(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio.index(pin) def _gpio_get_pin(self, pin): if (GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BOARD') or (GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BCM'): return pin elif GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BCM': return self._gpio_bcm_to_board(pin) elif GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BOARD': return self._gpio_board_to_bcm(pin) else: return 0 def _configure_gpio(self): if not self._hasGPIO: self._logger.error("RPi.GPIO is required.") return self._logger.info("Running RPi.GPIO version %s" % GPIO.VERSION) if GPIO.VERSION < "0.6": self._logger.error("RPi.GPIO version 0.6.0 or greater required.") GPIO.setwarnings(False) for pin in self._configuredGPIOPins: self._logger.debug("Cleaning up pin %s" % pin) try: GPIO.cleanup(self._gpio_get_pin(pin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._configuredGPIOPins = [] if GPIO.getmode() is None: if self.GPIOMode == 'BOARD': GPIO.setmode(GPIO.BOARD) elif self.GPIOMode == 'BCM': GPIO.setmode(GPIO.BCM) else: return 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, pull_up_down=pudsenseGPIOPin) self._configuredGPIOPins.append(self.senseGPIOPin) except (RuntimeError, ValueError) as e: self._logger.error(e) if self.switchingMethod == 'GPIO': self._logger.info("Using GPIO for On/Off") self._logger.info("Configuring GPIO for pin %s" % self.onoffGPIOPin) try: 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): self._check_psu_state_event.set() def _check_psu_state(self): while True: old_isPSUOn = self.isPSUOn if self.sensingMethod == 'GPIO': if not self._hasGPIO: return self._logger.debug("Polling PSU state...") r = 0 try: r = GPIO.input(self._gpio_get_pin(self.senseGPIOPin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._logger.debug("Result: %s" % r) if r==1: new_isPSUOn = True elif r==0: 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, shell=True) self._logger.debug("Sensing system command executed. PID=%s, Command=%s" % (p.pid, self.senseSystemCommand)) while p.poll() is None: time.sleep(0.1) r = p.returncode self._logger.debug("Sensing system command returned: %s" % r) if r==0: new_isPSUOn = True elif r==1: new_isPSUOn = False self.isPSUOn = new_isPSUOn elif self.sensingMethod == 'INTERNAL': self.isPSUOn = self._noSensing_isPSUOn else: return self._logger.debug("isPSUOn: %s" % self.isPSUOn) if (old_isPSUOn != self.isPSUOn) and self.isPSUOn: self._start_idle_timer() elif (old_isPSUOn != self.isPSUOn) and not self.isPSUOn: self._stop_idle_timer() self._plugin_manager.send_plugin_message(self._identifier, dict(hasGPIO=self._hasGPIO, isPSUOn=self.isPSUOn)) self._check_psu_state_event.wait(5) self._check_psu_state_event.clear() def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle and self.isPSUOn: self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._printer.is_printing() or self._printer.is_paused(): return self._logger.info("Idle timeout reached after %s minute(s). Turning heaters off prior to shutting off PSU." % self.idleTimeout) if self._wait_for_heaters(): self._logger.info("Heaters below temperature.") self.turn_psu_off() else: self._logger.info("Aborted PSU shut down due to activity.") def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater in heaters.keys(): if heater == 'bed': continue self._logger.info("Getting temperature of %s heater " % (heater)) temp = float(heaters.get(heater)["actual"]) self._logger.debug("Heater %s = %sC" % (heater,temp)) if float(heaters.get(heater)["target"]) != 0: self._logger.info("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater in heaters.keys(): if heater == 'bed': continue temp = float(heaters.get(heater)["actual"]) self._logger.debug("Heater %s = %sC" % (heater,temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._logger.info("Waiting for heaters(%s) before shutting off PSU..." % ', '.join(heaters_above_waittemp)) time.sleep(5)
class LightControl(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): global GPIO import RPi.GPIO as GPIO self._pin_to_gpio_rev1 = [-1, -1, -1, 0, -1, 1, -1, 4, 14, -1, 15, 17, 18, 21, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] self._pin_to_gpio_rev2 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] self._pin_to_gpio_rev3 = [-1, -1, -1, 2, -1, 3, -1, 4, 14, -1, 15, 17, 18, 27, -1, 22, 23, -1, 24, 10, -1, 9, 25, 11, 8, -1, 7, -1, -1, 5, -1, 6, 12, 13, -1, 19, 16, 26, 20, -1, 21] self.GPIOMode = '' self.onoffGPIOPin = 0 self.invertonoffGPIOPin = False self.powerOffWhenIdle = False self.isLightOn = False self.idleTimeout = 0 self._idleTimer = None self._configuredGPIOPins = [] def on_settings_initialized(self): self.GPIOMode = self._settings.get(["GPIOMode"]) self._logger.debug("GPIOMode: %s" % self.GPIOMode) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self._logger.debug("onoffGPIOPin: %s" % self.onoffGPIOPin) self.invertonoffGPIOPin = \ self._settings.get_boolean(["invertonoffGPIOPin"]) self._logger.debug("invertonoffGPIOPin: %s" % self.invertonoffGPIOPin) self.powerOffWhenIdle = \ self._settings.get_boolean(["powerOffWhenIdle"]) self._logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._logger.debug("idleTimeout: %s" % self.idleTimeout) self._configure_gpio() self.turn_light_off() self._start_idle_timer() def _gpio_board_to_bcm(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio[pin] def _gpio_bcm_to_board(self, pin): if GPIO.RPI_REVISION == 1: pin_to_gpio = self._pin_to_gpio_rev1 elif GPIO.RPI_REVISION == 2: pin_to_gpio = self._pin_to_gpio_rev2 else: pin_to_gpio = self._pin_to_gpio_rev3 return pin_to_gpio.index(pin) def _gpio_get_pin(self, pin): if (GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BOARD') \ or (GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BCM'): return pin elif GPIO.getmode() == GPIO.BOARD and self.GPIOMode == 'BCM': return self._gpio_bcm_to_board(pin) elif GPIO.getmode() == GPIO.BCM and self.GPIOMode == 'BOARD': return self._gpio_board_to_bcm(pin) else: return 0 def _configure_gpio(self): self._logger.info("Running RPi.GPIO version %s" % GPIO.VERSION) if GPIO.VERSION < "0.6": self._logger.error("RPi.GPIO version 0.6.0 or greater required.") GPIO.setwarnings(False) for pin in self._configuredGPIOPins: self._logger.debug("Cleaning up pin %s" % pin) try: GPIO.cleanup(self._gpio_get_pin(pin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._configuredGPIOPins = [] if GPIO.getmode() is None: if self.GPIOMode == 'BOARD': GPIO.setmode(GPIO.BOARD) elif self.GPIOMode == 'BCM': GPIO.setmode(GPIO.BCM) else: return self._logger.info("Using GPIO for On/Off") self._logger.info("Configuring GPIO for pin %s" % self.onoffGPIOPin) try: 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 _start_idle_timer(self): self._logger.debug('Starting idle timer') if self._idleTimer: self._logger.debug('Resetting idle timer') self._reset_idle_timer() else: if self.powerOffWhenIdle and self.isLightOn: self._logger.debug('Starting idle timer with timeout %d' % self.idleTimeout) self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except Exception as e: self._logger.exception(e) self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return self._logger.info("Idle timeout reached after %s minute(s). " "shutting off Light." % self.idleTimeout) self.turn_light_off(True) self._stop_idle_timer() def turn_light_on(self): self._logger.debug("Switching Light On Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = GPIO.HIGH else: pin_output = GPIO.LOW try: GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) self.isLightOn = True self._logger.debug('Sending plugin message as %s' % self._identifier) self._plugin_manager.send_plugin_message(self._identifier, {'hasGPIO': True, 'isLightOn': True}) self._start_idle_timer() except (RuntimeError, ValueError) as e: self._logger.error(e) def turn_light_off(self, idleOff=False): self._logger.debug("Switching Light Off Using GPIO: %s" % self.onoffGPIOPin) if not self.invertonoffGPIOPin: pin_output = GPIO.LOW else: pin_output = GPIO.HIGH try: GPIO.output(self._gpio_get_pin(self.onoffGPIOPin), pin_output) self.isLightOn = False self._logger.debug('Sending plugin message as %s' % self._identifier) self._plugin_manager.send_plugin_message(self._identifier, {'hasGPIO': True, 'isLightOn': False}) if not idleOff: self._stop_idle_timer() except (RuntimeError, ValueError) as e: self._logger.error(e) def get_api_commands(self): return dict( turnLightOn=[], turnLightOff=[], toggleLight=[], getLightState=[] ) def on_api_command(self, command, data): self._logger.info('Received command %s' % command) if not user_permission.can(): return make_response("Insufficient rights", 403) if command == 'turnLightOn': self.turn_light_on() elif command == 'turnLightOff': self.turn_light_off() elif command == 'toggleLight': if self.isLightOn: self.turn_light_off() else: self.turn_light_on() elif command == 'getLightState': return jsonify(isLightOn=self.isLightOn) else: self._logger.warn('Received unexpected command %s' % command) def get_settings_defaults(self): return dict( GPIOMode='BOARD', onoffGPIOPin=0, invertonoffGPIOPin=False, powerOffWhenIdle=False, idleTimeout=5, ) def on_settings_save(self, data): old_GPIOMode = self.GPIOMode old_onoffGPIOPin = self.onoffGPIOPin octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.GPIOMode = self._settings.get(["GPIOMode"]) self.onoffGPIOPin = self._settings.get_int(["onoffGPIOPin"]) self.invertonoffGPIOPin = \ self._settings.get_boolean(["invertonoffGPIOPin"]) self.powerOffWhenIdle = \ self._settings.get_boolean(["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) if old_GPIOMode != self.GPIOMode or old_onoffGPIOPin != self.onoffGPIOPin: self._configure_gpio() self._start_idle_timer() def get_settings_version(self): return 1 def on_settings_migrate(self, target, current=None): pass def get_template_configs(self): return [ dict(type="settings", custom_bindings=True) ] def get_assets(self): return { "js": ["js/lightcontrol.js"], "less": ["less/lightcontrol.less"], "css": ["css/lightcontrol.min.css"] }