def on_api_command(self, command, data):
        if command == "select":
            if self._procastinating is not True:
                return

            self._plugin_manager.send_plugin_message(
                self._identifier, dict(action="dialog:close"))
            choice = data["choice"]
            if choice == "NOW":
                self._procastinating = False
                self._printer.set_job_on_hold(False)
            else:
                try:
                    now = datetime.strptime(
                        datetime.now().strftime("%H:%M:%S"), "%H:%M:%S")
                    work = datetime.strptime(choice + ":00", "%H:%M:%S")
                    delay = (work - now).seconds
                    if delay < 0:
                        delay += 86400
                    self._logger.info(
                        "Procastinating for {0} seconds".format(delay))
                    self.timer = ResettableTimer(
                        delay, self.on_timedout)  #, (self, ))
                    self.timer.start()
                    self._plugin_manager.send_plugin_message(
                        self._identifier,
                        dict(action="notice:show",
                             template="notice:until",
                             parameters=choice))
                except ValueError:
                    self._logger.error(
                        "on_api_command() invoked with invalid choice='{0}'".
                        format(choice))
                    self._printer.set_job_on_hold(False)
                    self._procastinating = False
    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 _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()
Example #4
0
    def _restart_timer(self):
        # Stop timers in progress.
        self._stop_timers()

        # Get the current settings.
        enabled = self._settings.get_boolean(['enabled'])
        initial_delay = self._settings.get_int(['initial_delay'])
        poll_period = self._settings.get_int(['poll_period'])
        num_offline = self._settings.get_int(['num_offline'])

        self._logger.info(
            "Restarting: enabled=%r, initial_delay=%r, poll_period=%r, num_offline=%r"
            % (enabled, initial_delay, poll_period, num_offline))

        # If the plugin is enabled, then start up our periodic checks
        # of the serial connection after dealing with any initial delay.
        if (enabled == True):
            self._initial_timer = ResettableTimer(initial_delay,
                                                  self._start_periodic_timer, (
                                                      poll_period,
                                                      num_offline,
                                                  ))
            self._initial_timer.start()
        else:
            self._logger.info("Checking is disabled. Not restarting.")
Example #5
0
class PendingDecision(object):
    def __init__(self,
                 app_id,
                 app_token,
                 user_id,
                 user_token,
                 timeout_callback=None):
        self.app_id = app_id
        self.app_token = app_token
        self.user_id = user_id
        self.user_token = user_token
        self.created = monotonic_time()

        if callable(timeout_callback):
            self.poll_timeout = ResettableTimer(POLL_TIMEOUT, timeout_callback,
                                                [user_token])
            self.poll_timeout.start()

    def external(self):
        return dict(app_id=self.app_id,
                    user_id=self.user_id,
                    user_token=self.user_token)

    def __repr__(self):
        return u"PendingDecision({!r}, {!r}, {!r}, {!r}, timeout_callback=...)".format(
            self.app_id, self.app_token, self.user_id, self.user_token)
Example #6
0
	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()
Example #7
0
    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 _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 test_function(self):
		timer_task = mock.MagicMock()

		timer = ResettableTimer(10, timer_task)
		timer.start()

		# wait for it
		timer.join()

		self.assertEqual(1, timer_task.call_count)
Example #10
0
    def __init__(self, app_id, app_token, user_id, user_token, timeout_callback=None):
        self.app_id = app_id
        self.app_token = app_token
        self.user_id = user_id
        self.user_token = user_token
        self.created = monotonic_time()

        if callable(timeout_callback):
            self.poll_timeout = ResettableTimer(
                POLL_TIMEOUT, timeout_callback, [user_token]
            )
            self.poll_timeout.start()
Example #11
0
 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()
Example #12
0
    def print_end(self, forced):
        self._logger.info("print_end - forced={}".format(forced))

        self.streaming = False

        if forced:
            self.stream_end()
        else:
            if self.end_timer:
                self.end_timer.reset()
            else:
                self.end_timer = ResettableTimer(60.0, self.stream_end)
                self.end_timer.start()
Example #13
0
	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)
Example #14
0
class PendingDecision(object):
	def __init__(self, app_id, app_token, user_id, user_token, timeout_callback=None):
		self.app_id = app_id
		self.app_token = app_token
		self.user_id = user_id
		self.user_token = user_token
		self.created = monotonic_time()

		if callable(timeout_callback):
			self.poll_timeout = ResettableTimer(POLL_TIMEOUT, timeout_callback, [user_token])
			self.poll_timeout.start()

	def external(self):
		return dict(app_id=self.app_id,
		            user_id=self.user_id,
		            user_token=self.user_token)
Example #15
0
	def __init__(self, app_id, app_token, user_id, user_token, timeout_callback=None):
		self.app_id = app_id
		self.app_token = app_token
		self.user_id = user_id
		self.user_token = user_token
		self.created = monotonic_time()

		if callable(timeout_callback):
			self.poll_timeout = ResettableTimer(POLL_TIMEOUT, timeout_callback, [user_token])
			self.poll_timeout.start()
    def update(self, printer_state):
        """Activate or deactivate the timer depending on the printer state.
        
        If the printer_state is less than or equal to the
        'display_timeout_option' setting, then the timer is active and
        the display may shutdown. While the printer_state is greater
        than the 'display_timeout_option' setting, the display will
        not shut down.

        """
        self.last_printer_state = printer_state
        if printer_state <= self.mode:
            if not self.timer:
                self.timer = ResettableTimer(self.timeout * 60,
                                             self.sleep,
                                             daemon=True)
                self.timer.start()
        else:
            self.cancel()
            if self.blank:
                self.wake()
	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 test_reset_callback(self):
        timer_task = mock.MagicMock()
        on_reset_cb = mock.MagicMock()

        timer = ResettableTimer(10, timer_task, on_reset=on_reset_cb)
        timer.start()

        timer.reset()

        # wait for it
        timer.join()

        self.assertEqual(1, timer_task.call_count)
        self.assertEqual(1, on_reset_cb.call_count)
    def test_function(self):
        timer_task = mock.MagicMock()

        timer = ResettableTimer(10, timer_task)
        timer.start()

        # wait for it
        timer.join()

        self.assertEqual(1, timer_task.call_count)
Example #20
0
	def test_reset_callback(self):
		timer_task = mock.MagicMock()
		on_reset_cb = mock.MagicMock()

		timer = ResettableTimer(10, timer_task, on_reset=on_reset_cb)
		timer.start()

		timer.reset()

		# wait for it
		timer.join()

		self.assertEqual(1, timer_task.call_count)
		self.assertEqual(1, on_reset_cb.call_count)
Example #21
0
    def print_stopped(self):
        for n in range(1, 9):
            index = "r" + str(n)
            settings = self.get_settings_defaults()[index]
            settings.update(self._settings.get([index]))

            relay_pin = int(settings["relay_pin"])
            inverted = settings['inverted_output']
            autoOFFforPrint = settings['autoOFFforPrint']
            autoOffDelay = int(settings['autoOffDelay'])
            if autoOFFforPrint:
                self._logger.debug(
                    "turn off pin: {} in {} seconds. index: {}".format(
                        relay_pin, autoOffDelay, index))
                self.turn_off_timers[index] = ResettableTimer(
                    autoOffDelay, self.turn_off_pin, [relay_pin, inverted])
                self.turn_off_timers[index].start()
        self.update_ui()
Example #22
0
    def print_stopped(self):
        for index in self.model:
            settings = self.get_settings_defaults()[index]
            settings.update(self._settings.get([index]))

            relay_pin = int(settings["relay_pin"])
            inverted = settings['inverted_output']
            autoOFFforPrint = settings['autoOFFforPrint']
            autoOffDelay = int(settings['autoOffDelay'])
            cmdOFF = settings['cmdOFF']
            active = settings["active"]
            if autoOFFforPrint and active:
                self._logger.debug(
                    "turn off pin: {} in {} seconds. index: {}".format(
                        relay_pin, autoOffDelay, index))
                self.turn_off_timers[index] = ResettableTimer(
                    autoOffDelay, self.turn_off_pin,
                    [relay_pin, inverted, cmdOFF])
                self.turn_off_timers[index].start()
        self.update_ui()
    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)
Example #24
0
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"
			)
		)
Example #25
0
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"
        ))
Example #26
0
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 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)
Example #28
0
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"
        ))
Example #30
0
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 ProcastinatorPlugin(octoprint.plugin.AssetPlugin,
                          octoprint.plugin.EventHandlerPlugin,
                          octoprint.plugin.SettingsPlugin,
                          octoprint.plugin.SimpleApiPlugin,
                          octoprint.plugin.TemplatePlugin):

    # noinspection PyMissingConstructor
    def __init__(self):
        self._procastinating = False
        self._worktimes = list()

    def initialize(self):
        self._procastinating = False
        self._worktimes = list()

    def get_assets(self):
        return dict(js=["js/procastinator.js", "clientjs/procastinator.js"])

    def get_update_information(self):
        return dict(procastinator=dict(
            displayName=__plugin_name__,
            displayVersion=self._plugin_version,
            type="github_release",
            user="******",
            repo="OctoPrint-Procastinator",
            current=self._plugin_version,
            pip=
            "https://github.com/juergenpabel/OctoPrint-Procastinator/archive/{target_version}.zip"
        ))

    #~ EventHandlerPlugin
    def on_event(self, event, payload):
        if event == Events.PRINT_STARTED:
            if self._settings.get_boolean(["enabled"]):
                now = datetime.strptime(datetime.now().strftime("%H:%M:%S"),
                                        "%H:%M:%S")
                start = datetime.strptime(
                    self._settings.get(["starttime"]) + ":00", "%H:%M:%S")
                end = datetime.strptime(
                    self._settings.get(["endtime"]) + ":00", "%H:%M:%S")
                if (start < end and (now >= start and now <= end)) or (
                        start > end and (now > start or now < end)):
                    self._procastinating = True
                    self._printer.set_job_on_hold(True)
                    self._worktimes = list()
                    self._worktimes.append("NOW")
                    for worktime in self._settings.get(["worktimes"]):
                        if worktime['time'] is not None:
                            self._worktimes.append(worktime['time'])
                    self._plugin_manager.send_plugin_message(
                        self._identifier,
                        dict(action="dialog:show",
                             template="dialog:choice",
                             parameters=self._worktimes))
        if event == Events.PRINT_DONE:
            self._procastinating = False
            self._plugin_manager.send_plugin_message(
                self._identifier, dict(action="notice:close"))
        if event == Events.DISCONNECTED:
            self._procastinating = False
            self._plugin_manager.send_plugin_message(
                self._identifier, dict(action="dialog:close"))
            self._plugin_manager.send_plugin_message(
                self._identifier, dict(action="notice:close"))
        pass

    #~ SettingsPlugin
    def get_settings_defaults(self):
        return dict(enabled=False,
                    starttime="00:00",
                    endtime="23:59",
                    worktimes=list(({
                        'time': None
                    }, {
                        'time': None
                    }, {
                        'time': None
                    }, {
                        'time': None
                    }, {
                        'time': None
                    })))

    #~ SimpleApiPlugin
    def get_api_commands(self):
        return dict(select=["choice"])

    def on_timedout(self):
        self._procastinating = False
        self._printer.set_job_on_hold(False)
        self._plugin_manager.send_plugin_message(self._identifier,
                                                 dict(action="dialog:close"))
        self._plugin_manager.send_plugin_message(
            self._identifier,
            dict(action="notice:show", template="notice:continue"))

    def on_api_command(self, command, data):
        if command == "select":
            if self._procastinating is not True:
                return

            self._plugin_manager.send_plugin_message(
                self._identifier, dict(action="dialog:close"))
            choice = data["choice"]
            if choice == "NOW":
                self._procastinating = False
                self._printer.set_job_on_hold(False)
            else:
                try:
                    now = datetime.strptime(
                        datetime.now().strftime("%H:%M:%S"), "%H:%M:%S")
                    work = datetime.strptime(choice + ":00", "%H:%M:%S")
                    delay = (work - now).seconds
                    if delay < 0:
                        delay += 86400
                    self._logger.info(
                        "Procastinating for {0} seconds".format(delay))
                    self.timer = ResettableTimer(
                        delay, self.on_timedout)  #, (self, ))
                    self.timer.start()
                    self._plugin_manager.send_plugin_message(
                        self._identifier,
                        dict(action="notice:show",
                             template="notice:until",
                             parameters=choice))
                except ValueError:
                    self._logger.error(
                        "on_api_command() invoked with invalid choice='{0}'".
                        format(choice))
                    self._printer.set_job_on_hold(False)
                    self._procastinating = False

    def on_api_get(self, request):
        if self._procastinating is not True:
            return flask.jsonify()
        else:
            return flask.jsonify(action="dialog:show",
                                 template="dialog:choice",
                                 parameters=self._worktimes)

    #~ TemplatePlugin
    def get_template_configs(self):
        return [
            dict(type="settings", name="Procastinator", custom_bindings=False)
        ]
Example #32
0
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 DisplayTimer:
    """Coordination class for display timeout.
    """
    def __init__(self, settings, panel):
        self.panel = panel
        self.timer = None
        self.blank = False
        self.last_printer_state = 0
        self.setup(settings)

    def setup(self, settings):
        """Adjust settings when changed by the user.
        """
        self.timeout = settings.get_int(['display_timeout_time'], merged=True)
        self.mode = settings.get_int(['display_timeout_option'], merged=True)
        self.cancel()
        self.update(self.last_printer_state)

    def cancel(self):
        """Cancel the running timer.
        """
        if self.timer:
            self.timer.cancel()
            self.timer = None

    def update(self, printer_state):
        """Activate or deactivate the timer depending on the printer state.
        
        If the printer_state is less than or equal to the
        'display_timeout_option' setting, then the timer is active and
        the display may shutdown. While the printer_state is greater
        than the 'display_timeout_option' setting, the display will
        not shut down.

        """
        self.last_printer_state = printer_state
        if printer_state <= self.mode:
            if not self.timer:
                self.timer = ResettableTimer(self.timeout * 60,
                                             self.sleep,
                                             daemon=True)
                self.timer.start()
        else:
            self.cancel()
            if self.blank:
                self.wake()

    def sleep(self):
        """Put the display to sleep and deactivate the timer.
        """
        logger.info(f'Display blanking after {self.timeout} min timeout')
        self.blank = True
        self.panel.poweroff()
        self.cancel()

    def wake(self):
        """Activate the display and restore the timer based on the last
        known printer state.
        """
        logger.info('Display powering on')
        self.blank = False
        self.panel.poweron()
        self.update(self.last_printer_state)

    def poke(self):
        """Reset the timer's timeout.

        Typically called as part of a button press handler.
        """
        if self.timer:
            self.timer.reset()

    @property
    def is_blank(self):
        return self.blank
Example #34
0
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"
        ))
Example #35
0
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]