def test_cancelled_callback(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step on_cancelled = mock.MagicMock() on_condition_false = mock.MagicMock() timer = RepeatedTimer( 10, timer_task, condition=lambda: countdown.counter > 0, on_condition_false=on_condition_false, on_cancelled=on_cancelled, ) timer.start() # give it some time to run time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() self.assertEqual(0, on_condition_false.call_count) self.assertEqual(1, on_cancelled.call_count)
class DisplayETAPlugin(octoprint.plugin.ProgressPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): self.activated = True self.timer = RepeatedTimer( 5.0, DisplayETAPlugin.fromTimer, args=[self], run_first=False, ) self.timer.start() self.currentFile = None def on_event(self, event, payload): if event == "FileSelected": self.currentFile = payload["file"] def fromTimer(self): self._logger.info("The Timer Has Fired") self._logger.info(self._printer.get_current_job()) self._printer.select_file( '/home/pablo/.octoprint/uploads/20mm_hollow_cube.gcode', False, printAfterSelect=True) self.timer.cancel() #self._plugin_manager.send_plugin_message(self._identifier, dict(eta_string=self.eta_string)) def get_assets(self): return {"js": ["js/displayeta.js"]}
class TimedTimelapse(Timelapse): def __init__(self, post_roll=0, interval=1, fps=25): Timelapse.__init__(self, post_roll=post_roll, fps=fps) self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s self._postroll_captures = 0 self._timer = None self._logger.debug("TimedTimelapse initialized") @property def interval(self): return self._interval def config_data(self): return { "type": "timed", "options": { "interval": self._interval } } def on_print_started(self, event, payload): Timelapse.on_print_started(self, event, payload) if self._timer is not None: return self._logger.debug("Starting timer for interval based timelapse") from octoprint.util import RepeatedTimer self._timer = RepeatedTimer(self._interval, self._timer_task, run_first=True, condition=self._timer_active, on_finish=self._on_timer_finished) self._timer.start() def on_print_done(self, event, payload): self._postroll_captures = self.post_roll * self.fps Timelapse.on_print_done(self, event, payload) def calculate_post_roll(self): return self.post_roll * self.fps * self.interval def process_post_roll(self): pass def post_roll_finished(self): Timelapse.post_roll_finished(self) self._timer = None def _timer_active(self): return self._in_timelapse or self._postroll_captures > 0 def _timer_task(self): self.captureImage() if self._postroll_captures > 0: self._postroll_captures -= 1 def _on_timer_finished(self): self.post_roll_finished()
class TimedTimelapse(Timelapse): def __init__(self, post_roll=0, interval=1, fps=25): Timelapse.__init__(self, post_roll=post_roll, fps=fps) self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s self._postroll_captures = 0 self._timer = None self._logger.debug("TimedTimelapse initialized") @property def interval(self): return self._interval def config_data(self): return { "type": "timed", "options": { "interval": self._interval } } def on_print_started(self, event, payload): Timelapse.on_print_started(self, event, payload) if self._timer is not None: return self._logger.debug("Starting timer for interval based timelapse") from octoprint.util import RepeatedTimer self._timer = RepeatedTimer(self._interval, self._timer_task, run_first=True, condition=self._timer_active, on_finish=self._on_timer_finished) self._timer.start() def on_print_done(self, event, payload): self._postroll_captures = self.post_roll * self.fps Timelapse.on_print_done(self, event, payload) def calculate_post_roll(self): return self.post_roll * self.fps * self.interval def process_post_roll(self): pass def post_roll_finished(self): Timelapse.post_roll_finished(self) self._timer = None def _timer_active(self): return self._in_timelapse or self._postroll_captures > 0 def _timer_task(self): self.capture_image() if self._postroll_captures > 0: self._postroll_captures -= 1 def _on_timer_finished(self): self.post_roll_finished()
def _settimer(timervar, timeval, methodcall): worktimer = None if timervar is not None: timervar.cancel() worktimer = RepeatedTimer(timeval, methodcall, None, None, True) worktimer.start() return worktimer
def test_condition(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step timer = RepeatedTimer(0.1, timer_task, condition=lambda: countdown.counter > 0) timer.start() # wait for it timer.join() self.assertEqual(5, timer_task.call_count)
def test_condition(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step timer = RepeatedTimer(0.1, timer_task, condition=lambda: countdown.counter > 0) timer.start() # wait for it timer.join() self.assertEqual(5, timer_task.call_count)
class NetworkHealthPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.RestartNeedingPlugin): def __init__(self): self._timer = None def on_after_startup(self): self._logger.info("Beginning to monitor Network health...") self._timer = RepeatedTimer(180, self._check_network) self._timer.start() def _check_network(self): try: if not check_ping(): self._logger.error("No Network Connection - Resetting Adapter(s)...") reset_wlan0 = 'sudo ip link set wlan0 down; sudo ip link set wlan0 up' reset_eth0 = 'sudo ip link set eth0 down; sudo ip link set eth0 up' os.system(reset_wlan0) os.system(reset_eth0) except Exception: self._logger.exception("Could not run network health check") def get_update_information(self, *args, **kwargs): return dict( networkhealth=dict( displayName=self._plugin_name, displayVersion=self._plugin_version, type="github_release", current=self._plugin_version, user="******", repo="OctoPrint-NetworkHealth", pip="https://github.com/jonfairbanks/OctoPrint-NetworkHealth/archive/{target}.zip" ) ) def default_gateway(): import socket import struct with open("/proc/net/route") as fh: for line in fh: fields = line.strip().split() if fields[1] != '00000000' or not int(fields[3], 16) & 2: continue return socket.inet_ntoa(struct.pack("<L", int(fields[2], 16))) def check_ping(): hostname = default_gateway() response = os.system("ping -c 4 " + hostname) if response == 0: return True else: return False
def test_not_run_first(self): timer_task = mock.MagicMock() timer = RepeatedTimer(60, timer_task) timer.start() # give it some time to run - should hang in the sleep phase though time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() self.assertEqual(0, timer_task.call_count)
def start_repeated_timer(self, timer=None, callback=None): try: if timer is None and callback is not None: self._logger.debug("creating repeated timer") timer = RepeatedTimer( self.polling_interval, callback, run_first=True, condition=self._continue_polling, on_condition_false=self._polling_canceled) timer.start() return True, timer except Exception: return False, timer
def test_not_run_first(self): timer_task = mock.MagicMock() timer = RepeatedTimer(60, timer_task) timer.start() # give it some time to run - should hang in the sleep phase though time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() self.assertEqual(0, timer_task.call_count)
def test_finished_callback(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step on_finished = mock.MagicMock() timer = RepeatedTimer(0.1, timer_task, condition=lambda: countdown.counter > 0, on_finish=on_finished) timer.start() # wait for it timer.join() self.assertEqual(1, on_finished.call_count)
def test_finished_callback(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step on_finished = mock.MagicMock() timer = RepeatedTimer(0.1, timer_task, condition=lambda: countdown.counter > 0, on_finish=on_finished) timer.start() # wait for it timer.join() self.assertEqual(1, on_finished.call_count)
def test_condition_change_during_task(self): def sleep(): time.sleep(2) timer_task = mock.MagicMock() timer_task.side_effect = sleep timer = RepeatedTimer(0.1, timer_task, run_first=True) timer.start() time.sleep(1) timer.condition = lambda: False timer.join() self.assertEqual(1, timer_task.call_count)
def test_run_first(self): timer_task = mock.MagicMock() timer = RepeatedTimer(60, timer_task, run_first=True) timer.start() # give it some time to run time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() # should have run once self.assertEqual(1, timer_task.call_count)
def test_run_first(self): timer_task = mock.MagicMock() timer = RepeatedTimer(60, timer_task, run_first=True) timer.start() # give it some time to run time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() # should have run once self.assertEqual(1, timer_task.call_count)
def test_condition_change_during_task(self): def sleep(): time.sleep(2) timer_task = mock.MagicMock() timer_task.side_effect = sleep timer = RepeatedTimer(0.1, timer_task, run_first=True) timer.start() time.sleep(1) timer.condition = lambda: False timer.join() self.assertEqual(1, timer_task.call_count)
def on_event(self, event, payload): if event == octoprint.events.Events.CONNECTED: self._logger.info('Printer connected') self.connected = True t = RepeatedTimer(5.0, self.send_M407_command, run_first=True, condition=self.timer_condition) t.start() elif event == octoprint.events.Events.PRINT_STARTED: self._logger.info('print started') elif event == octoprint.events.Events.PRINT_DONE: self._logger.info('print done') elif event == octoprint.events.Events.PRINT_FAILED or event == octoprint.events.Events.PRINT_CANCELLED: #self.connected = False self._logger.info('print failed or canceled') elif event == octoprint.events.Events.DISCONNECTED: self.connected = False self._logger.info('Printer disconnected')
class TimedTimelapse(Timelapse): def __init__(self, interval=1, post_roll=0, fps=25): Timelapse.__init__(self, post_roll=post_roll, fps=fps) self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s self._timer = None self._logger.debug("TimedTimelapse initialized") @property def interval(self): return self._interval def config_data(self): return { "type": "timed", "options": { "interval": self._interval } } def on_print_started(self, event, payload): Timelapse.on_print_started(self, event, payload) if self._timer is not None: return self._logger.debug("Starting timer for interval based timelapse") from octoprint.util import RepeatedTimer self._timer = RepeatedTimer(self._interval, self._timer_task, run_first=True, condition=self._timer_active, on_finish=self._on_timer_finished) self._timer.start() def process_post_roll(self): # we only use the final image as post roll self._copying_postroll() self.post_roll_finished() def _timer_active(self): return self._in_timelapse def _timer_task(self): self.capture_image() def _on_timer_finished(self): # timer is done, delete it self._timer = None
class TimedTimelapse(Timelapse): def __init__(self, interval=1, post_roll=0, fps=25): Timelapse.__init__(self, post_roll=post_roll, fps=fps) self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s self._timer = None self._logger.debug("TimedTimelapse initialized") @property def interval(self): return self._interval def config_data(self): return {"type": "timed", "options": {"interval": self._interval}} def on_print_started(self, event, payload): Timelapse.on_print_started(self, event, payload) if self._timer is not None: return self._logger.debug("Starting timer for interval based timelapse") from octoprint.util import RepeatedTimer self._timer = RepeatedTimer( self._interval, self._timer_task, run_first=True, condition=self._timer_active, on_finish=self._on_timer_finished, ) self._timer.start() def process_post_roll(self): # we only use the final image as post roll self._copying_postroll() self.post_roll_finished() def _timer_active(self): return self._in_timelapse def _timer_task(self): self.capture_image() def _on_timer_finished(self): # timer is done, delete it self._timer = None
def updateCmds(self): for timer in self.cmd_timers: timer.cancel() del self.cmd_timers[:] index = 0 for command in self.cmd_commands: if (command.get("enabled")): t = RepeatedTimer(float(command.get("interval")), self.runCmd, [index], run_first=True) t.start() self.cmd_timers.append(t) index += 1
def updateCmds(self): self.cmd_commands = self._settings.get(["commandWidgetArray"]) for timer in self.cmd_timers: timer.cancel() del self.cmd_timers[:] if self._settings.get_boolean(['showCommandWidgets']): index = 0 for command in self.cmd_commands: if command.get("enabled"): t = RepeatedTimer(float(command.get("interval")), self.runCmd, [index], run_first=True) t.start() self.cmd_timers.append(t) index += 1
def test_adjusted_interval(self): increasing_interval = IncreasingInterval(3, 1) timer_task = mock.MagicMock() timer_task.side_effect = increasing_interval.step timer = RepeatedTimer(increasing_interval.interval, timer_task, condition=lambda: increasing_interval.counter > 0) # this should take 1 + 2 + 3 = 6s start_time = time.time() timer.start() timer.join() duration = time.time() - start_time self.assertEqual(3, timer_task.call_count) self.assertGreaterEqual(duration, 6) self.assertLess(duration, 7)
def test_adjusted_interval(self): increasing_interval = IncreasingInterval(3, 1) timer_task = mock.MagicMock() timer_task.side_effect = increasing_interval.step timer = RepeatedTimer(increasing_interval.interval, timer_task, condition=lambda: increasing_interval.counter > 0) # this should take 1 + 2 + 3 = 6s start_time = time.time() timer.start() timer.join() duration = time.time() - start_time self.assertEqual(3, timer_task.call_count) self.assertGreaterEqual(duration, 6) self.assertLess(duration, 7)
def _initialize_lcd(self): self._logger.info("Running RPi.GPIO version %s" % GPIO.VERSION) if GPIO.VERSION < "0.6": self._logger.error("RPi.GPIO version 0.6.0 or greater required.") GPIO.setwarnings(False) for pin in self._configuredGPIOPins: self._logger.debug("Cleaning up pin %s" % pin) try: GPIO.cleanup(self._gpio_get_pin(pin)) except (RuntimeError, ValueError) as e: self._logger.error(e) self._configuredGPIOPins = [] if GPIO.getmode() is None: GPIO.setmode(GPIO.BOARD) GPIO.setup(self._gpio_get_pin(self.pin_rs), GPIO.OUT) GPIO.setup(self._gpio_get_pin(self.pin_e), GPIO.OUT) GPIO.setup(self._gpio_get_pin(self.pin_d4), GPIO.OUT) GPIO.setup(self._gpio_get_pin(self.pin_d5), GPIO.OUT) GPIO.setup(self._gpio_get_pin(self.pin_d6), GPIO.OUT) GPIO.setup(self._gpio_get_pin(self.pin_d7), GPIO.OUT) self._configuredGPIOPins = [self._gpio_get_pin(self.pin_rs), self._gpio_get_pin(self.pin_e), self._gpio_get_pin(self.pin_d4), self._gpio_get_pin(self.pin_d5), self._gpio_get_pin(self.pin_d6), self._gpio_get_pin(self.pin_d7)] self._lcd_send_byte(0x33, self._lcd_cmd) self._lcd_send_byte(0x32, self._lcd_cmd) self._lcd_send_byte(0x28, self._lcd_cmd) self._lcd_send_byte(0x0C, self._lcd_cmd) self._lcd_send_byte(0x06, self._lcd_cmd) self._lcd_send_byte(0x01, self._lcd_cmd) printerstate = self._printer.get_state_string() self._line1 = printerstate.center(20) self.clear_lower_half() timer = RepeatedTimer(30, self._lcd_update()) timer.start()
class FinishedJobControlClass(octoprint.plugin.StartupPlugin): def on_after_startup(self): # self._logger.info("Started Timer...") self.startTimer(10.0) def startTimer(self, interval): self._checkTempTimer = RepeatedTimer(interval, self.checkHotEndTemp, None, None, True) self._checkTempTimer.start() def checkHotEndTemp(self): druckerDrucktGerade = self._printer.is_printing() if not druckerDrucktGerade: # self._logger.info("Drucker druckt nicht, Fan-Control soll Fan steuern") currentTemp = self._printer.get_current_temperatures() if float(currentTemp["tool0"]["actual"]) < 35: self._printer.commands("M106 S0") # self._logger.info("Ist unter 35 Grad, Setting Fan to 0") elif (float(currentTemp["tool0"]["actual"]) >= 35) and (float(currentTemp["tool0"]["actual"]) <= 100): self._printer.commands("M106 S60") # self._logger.info("Ist zwischen 30 Grad und 100, Setting Fan to 60") else: self._printer.commands("M106 S100")
def test_cancelled_callback(self): countdown = Countdown(5) timer_task = mock.MagicMock() timer_task.side_effect = countdown.step on_cancelled = mock.MagicMock() on_condition_false = mock.MagicMock() timer = RepeatedTimer(10, timer_task, condition=lambda: countdown.counter > 0, on_condition_false=on_condition_false, on_cancelled=on_cancelled) timer.start() # give it some time to run time.sleep(1) # then cancel it and wait for the thread to really finish timer.cancel() timer.join() self.assertEqual(0, on_condition_false.call_count) self.assertEqual(1, on_cancelled.call_count)
class BeeCom(MachineCom): STATE_WAITING_FOR_BTF = 21 STATE_PREPARING_PRINT = 22 STATE_HEATING = 23 STATE_SHUTDOWN = 24 _beeConn = None _beeCommands = None _responseQueue = queue.Queue() _statusQueue = queue.Queue() _monitor_print_progress = True _connection_monitor_active = True _prepare_print_thread = None def __init__(self, callbackObject=None, printerProfileManager=None): super(BeeCom, self).__init__(None, None, callbackObject, printerProfileManager) self._openConnection() # monitoring thread self._monitoring_active = True self.monitoring_thread = threading.Thread(target=self._monitor, name="comm._monitor") self.monitoring_thread.daemon = True self.monitoring_thread.start() def _openConnection(self): """ Opens a new connection using the BEEcom driver :return: True if the connection was successful """ if self._beeConn is None: self._beeConn = BeePrinterConn(self._connShutdownHook) self._changeState(self.STATE_CONNECTING) self._beeConn.connectToFirstPrinter() if self._beeConn.isConnected(): self._beeCommands = self._beeConn.getCommandIntf() # change to firmware if self._beeCommands.getPrinterMode() == 'Bootloader': # checks for firmware updates self.update_firmware() self._beeCommands.goToFirmware() # restart connection self._beeConn.reconnect() # post connection callback self._onConnected() return True else: return False def current_firmware(self): """ Gets the current firmware version :return: """ firmware_v = self.getCommandsInterface().getFirmwareVersion() if firmware_v is not None: return firmware_v else: return 'Not available' def update_firmware(self): """ Updates the printer firmware if the value in the firmware.properties file is different from the current printer firmware :return: if no printer is connected just returns void """ _logger = logging.getLogger() # get the latest firmware file for the connected printer conn_printer = self.getConnectedPrinterName() if conn_printer is None: return printer_id = conn_printer.replace(' ', '').lower() if printer_id: from os.path import isfile, join _logger.info("Checking for firmware updates...") firmware_path = settings().getBaseFolder('firmware') firmware_properties = parsePropertiesFile(join(firmware_path, 'firmware.properties')) firmware_file_name = firmware_properties['firmware.'+printer_id] if firmware_file_name is not None and isfile(join(firmware_path, firmware_file_name)): fname_parts = firmware_file_name.split('-') # gets the current firmware version curr_firmware = self.current_firmware() curr_firmware_parts = curr_firmware.split('-') if len(curr_firmware_parts) == 3 and curr_firmware is not "Not available": curr_version_parts = curr_firmware_parts[2].split('.') file_version_parts = fname_parts[2].split('.') if len(curr_version_parts) == 4 and len(file_version_parts) == 4: for i in xrange(3): if int(file_version_parts[i]) != int(curr_version_parts[i]): # version update found _logger.info("Updating printer firmware...") self.getCommandsInterface().flashFirmware(join(firmware_path, firmware_file_name), firmware_file_name) _logger.info("Firmware updated to %s" % fname_parts[2]) return elif curr_firmware == '0.0.0': # If curr_firmware is 0.0.0 it means something went wrong with a previous firmware update _logger.info("Updating printer firmware...") self.getCommandsInterface().flashFirmware(join(firmware_path, firmware_file_name), firmware_file_name) _logger.info("Firmware updated to %s" % fname_parts[2]) return else: _logger.error("No firmware file matching the configuration for printer %s found" % conn_printer) _logger.info("No firmware updates found") def sendCommand(self, cmd, cmd_type=None, processed=False, force=False): """ Sends a custom command through the open connection :param cmd: :param cmd_type: :param processed: :param force: :return: """ cmd = cmd.encode('ascii', 'replace') if not processed: cmd = comm.process_gcode_line(cmd) if not cmd: return #if self.isPrinting() and not self.isSdFileSelected(): # self._commandQueue.put((cmd, cmd_type)) if self.isOperational(): wait = None if "g" in cmd.lower(): wait = "3" resp = self._beeCommands.sendCmd(cmd, wait) if resp: # puts the response in the monitor queue self._responseQueue.put(resp) # logs the command reply with errors splits = resp.rstrip().split("\n") for r in splits: if "Error" in r: self._logger.warning(r) return True else: return False def close(self, is_error=False, wait=True, timeout=10.0, *args, **kwargs): """ Closes the connection to the printer if it's active :param is_error: :param wait: unused parameter (kept for interface compatibility) :param timeout: :param args: :param kwargs: :return: """ if self._beeCommands is not None: self._beeCommands.stopStatusMonitor() if self._beeConn is not None: self._beeConn.close() self._changeState(self.STATE_CLOSED) def _changeState(self, newState): if self._state == newState: return oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.on_comm_state_change(newState) def confirmConnection(self): """ Confirms the connection changing the internal state of the printer :return: """ if self._beeConn.isConnected(): if self._beeCommands.isPrinting(): self._changeState(self.STATE_PRINTING) elif self._beeCommands.isShutdown(): self._changeState(self.STATE_SHUTDOWN) else: self._changeState(self.STATE_OPERATIONAL) else: self._changeState(self.STATE_WAITING_FOR_BTF) def getConnectedPrinterName(self): """ Returns the current connected printer name :return: """ if self._beeConn is not None: return self._beeConn.getConnectedPrinterName() else: return "" def getConnectedPrinterSN(self): """ Returns the current connected printer serial number :return: """ if self._beeConn is not None: return self._beeConn.getConnectedPrinterSN() else: return None def isOperational(self): return self._state == self.STATE_OPERATIONAL \ or self._state == self.STATE_PRINTING \ or self._state == self.STATE_PAUSED \ or self._state == self.STATE_SHUTDOWN \ or self._state == self.STATE_TRANSFERING_FILE \ or self._state == self.STATE_PREPARING_PRINT \ or self._state == self.STATE_HEATING def isClosedOrError(self): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR \ or self._state == self.STATE_CLOSED or self._state == self.STATE_WAITING_FOR_BTF def isBusy(self): return self.isPrinting() or self.isPaused() or self.isPreparingPrint() def isPreparingPrint(self): return self._state == self.STATE_PREPARING_PRINT or self._state == self.STATE_HEATING def isPrinting(self): return self._state == self.STATE_PRINTING def isHeating(self): return self._state == self.STATE_HEATING def isShutdown(self): return self._state == self.STATE_SHUTDOWN def getStateString(self): """ Returns the current printer state :return: """ if self._state == self.STATE_WAITING_FOR_BTF or self._state == self.STATE_CLOSED: return "No printer detected. Please connect your printer" elif self._state == self.STATE_PREPARING_PRINT: return "Preparing to print, please wait" elif self._state == self.STATE_HEATING: return "Heating" elif self._state == self.STATE_SHUTDOWN: return "Shutdown" elif self._state == self.STATE_OPERATIONAL: return "Ready" else: return super(BeeCom, self).getStateString() def startPrint(self, pos=None): """ Starts the printing operation :param pos: unused parameter, just to keep the interface compatible with octoprint """ if not self.isOperational() or self.isPrinting(): return if self._currentFile is None: raise ValueError("No file selected for printing") try: self._changeState(self.STATE_PREPARING_PRINT) if self.isSdFileSelected(): print_resp = self._beeCommands.startSDPrint(self._currentFile.getFilename()) if print_resp: self._sd_status_timer = RepeatedTimer(self._timeout_intervals.get("sdStatus", 1.0), self._poll_sd_status, run_first=True) self._sd_status_timer.start() else: print_resp = self._beeCommands.printFile(self._currentFile.getFilename()) if print_resp is True: self._heatupWaitStartTime = time.time() self._heatupWaitTimeLost = 0.0 self._pauseWaitStartTime = 0 self._pauseWaitTimeLost = 0.0 self._heating = True self._prepare_print_thread = threading.Thread(target=self._preparePrintThread, name="comm._preparePrint") self._prepare_print_thread.daemon = True self._prepare_print_thread.start() else: self._errorValue = "Error while preparing the printing operation." self._logger.exception(self._errorValue) self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return except: self._errorValue = get_exception_string() self._logger.exception("Error while trying to start printing: " + self.getErrorString()) self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def cancelPrint(self, firmware_error=None): """ Cancels the print operation :type firmware_error: unused parameter, just to keep the interface compatible with octoprint """ if not self.isOperational() or self.isStreaming(): return if self._beeCommands.cancelPrint(): self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): if self._sd_status_timer is not None: try: self._sd_status_timer.cancel() except: pass # protects against any unexpected null selectedFile if self._currentFile is not None: payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "firmwareError": firmware_error } else: payload = { "file": None, "filename": '', "origin": '', "firmwareError": firmware_error } eventManager().fire(Events.PRINT_CANCELLED, payload) # sends usage statistics self._sendUsageStatistics('cancel') else: self._logger.exception("Error while canceling the print operation.") eventManager().fire(Events.ERROR, {"error": "Error canceling print"}) return def setPause(self, pause): """ Toggle Pause method :param pause: True to pause or False to unpause :return: """ if self.isStreaming(): return if not self._currentFile: return payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } if (not pause and self.isPaused()) or self.isShutdown(): if self._pauseWaitStartTime: self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) self._pauseWaitStartTime = None # resumes printing self._beeCommands.resumePrint() # restarts the progress monitor thread self.startPrintStatusProgressMonitor() self._changeState(self.STATE_PRINTING) eventManager().fire(Events.PRINT_RESUMED, payload) elif pause and self.isPrinting(): if not self._pauseWaitStartTime: self._pauseWaitStartTime = time.time() # pause print self._beeCommands.pausePrint() self._changeState(self.STATE_PAUSED) eventManager().fire(Events.PRINT_PAUSED, payload) def enterShutdownMode(self): """ Enters the printer shutdown mode :return: """ if self.isStreaming(): return if not self._currentFile: return payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } # enter shutdown mode self._beeCommands.enterShutdown() self._changeState(self.STATE_SHUTDOWN) eventManager().fire(Events.POWER_OFF, payload) def initSdCard(self): """ Initializes the SD Card in the printer :return: """ if not self.isOperational(): return self._beeCommands.initSD() if settings().getBoolean(["feature", "sdAlwaysAvailable"]): self._sdAvailable = True self.refreshSdFiles() self._callback.on_comm_sd_state_change(self._sdAvailable) def refreshSdFiles(self): """ Refreshes the list of available SD card files :return: """ if not self.isOperational() or self.isBusy(): return fList = self._beeCommands.getFileList() ##~~ SD file list if len(fList) > 0 and 'FileNames' in fList: for sdFile in fList['FileNames']: if comm.valid_file_type(sdFile, "machinecode"): if comm.filter_non_ascii(sdFile): self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) else: if not filename.startswith("/"): # file from the root of the sd -- we'll prepend a / filename = "/" + filename self._sdFiles.append((sdFile, 0)) continue def startFileTransfer(self, filename, localFilename, remoteFilename): """ Transfers a file to the printer's SD Card """ if not self.isOperational() or self.isBusy(): self._log("Printer is not operation or busy") return self._currentFile = comm.StreamingGcodeFileInformation(filename, localFilename, remoteFilename) self._currentFile.start() # starts the transfer self._beeCommands.transferSDFile(filename, localFilename) eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename}) self._callback.on_comm_file_transfer_started(remoteFilename, self._currentFile.getFilesize()) # waits for transfer to end while self._beeCommands.getTransferCompletionState() > 0: time.sleep(2) remote = self._currentFile.getRemoteFilename() payload = { "local": self._currentFile.getLocalFilename(), "remote": remote, "time": self.getPrintTime() } self._currentFile = None self._changeState(self.STATE_OPERATIONAL) self._callback.on_comm_file_transfer_done(remote) eventManager().fire(Events.TRANSFER_DONE, payload) self.refreshSdFiles() def startPrintStatusProgressMonitor(self): """ Starts the monitor thread that keeps track of the print progress :return: """ if self._beeCommands is not None: # starts the progress status thread self._beeCommands.startStatusMonitor(self._statusProgressQueueCallback) def selectFile(self, filename, sd): """ Overrides the original selectFile method to allow to select files when printer is busy. For example when reconnecting after connection was lost and the printer is still printing :param filename: :param sd: :return: """ if sd: if not self.isOperational(): # printer is not connected, can't use SD return self._sdFileToSelect = filename self.sendCommand("M23 %s" % filename) else: self._currentFile = comm.PrintingGcodeFileInformation(filename, offsets_callback=self.getOffsets, current_tool_callback=self.getCurrentTool) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) def getPrintProgress(self): """ Gets the current print progress :return: """ if self._currentFile is None: return None return self._currentFile.getProgress() def _getResponse(self): """ Auxiliar method to read the command response queue :return: """ if self._beeConn is None: return None try: ret = self._responseQueue.get() except: self._log("Exception raised while reading from command response queue: %s" % (get_exception_string())) self._errorValue = get_exception_string() return None if ret == '': #self._log("Recv: TIMEOUT") return '' try: self._log("Recv: %s" % sanitize_ascii(ret)) except ValueError as e: self._log("WARN: While reading last line: %s" % e) self._log("Recv: %r" % ret) return ret def triggerPrintFinished(self): """ This method runs the post-print job code :return: """ self._sdFilePos = 0 self._callback.on_comm_print_job_done() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime() }) if self._sd_status_timer is not None: try: self._sd_status_timer.cancel() except: pass # sends usage statistics self._sendUsageStatistics('stop') def _monitor(self): """ Monitor thread of responses from the commands sent to the printer :return: """ feedback_controls, feedback_matcher = comm.convert_feedback_controls(settings().get(["controls"])) feedback_errors = [] pause_triggers = comm.convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"])) #exits if no connection is active if not self._beeConn.isConnected(): return startSeen = False supportWait = settings().getBoolean(["feature", "supportWait"]) while self._monitoring_active: try: line = self._getResponse() if line is None: continue ##~~ debugging output handling if line.startswith("//"): debugging_output = line[2:].strip() if debugging_output.startswith("action:"): action_command = debugging_output[len("action:"):].strip() if action_command == "pause": self._log("Pausing on request of the printer...") self.setPause(True) elif action_command == "resume": self._log("Resuming on request of the printer...") self.setPause(False) elif action_command == "disconnect": self._log("Disconnecting on request of the printer...") self._callback.on_comm_force_disconnect() else: for hook in self._printer_action_hooks: try: self._printer_action_hooks[hook](self, line, action_command) except: self._logger.exception("Error while calling hook {} with action command {}".format(self._printer_action_hooks[hook], action_command)) continue else: continue ##~~ Error handling line = self._handleErrors(line) ##~~ process oks if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")): self._clear_to_send.set() self._long_running_command = False ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') \ or ' B:' in line or line.startswith('B:'): self._processTemperatures(line) self._callback.on_comm_temperature_update(self._temp, self._bedTemp) ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line and not self._sdAvailable: self._sdAvailable = True self.refreshSdFiles() self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.on_comm_sd_files(self._sdFiles) elif 'SD printing byte' in line and self.isSdPrinting(): # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) self._callback.on_comm_progress() elif 'File opened' in line and not self._ignore_select: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = regex_sdFileOpened.search(line) if self._sdFileToSelect: name = self._sdFileToSelect self._sdFileToSelect = None else: name = match.group(1) self._currentFile = comm.PrintingSdFileInformation(name, int(match.group(2))) elif 'File selected' in line: if self._ignore_select: self._ignore_select = False elif self._currentFile is not None: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.on_comm_file_selected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) elif 'Writing to file' in line: # answer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._changeState(self.STATE_PRINTING) self._clear_to_send.set() line = "ok" elif 'Done saving file' in line: self.refreshSdFiles() elif 'File deleted' in line and line.strip().endswith("ok"): # buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in # current versions self._clear_to_send.set() ##~~ Message handling elif line.strip() != '' \ and line.strip() != 'ok' and not line.startswith("wait") \ and not line.startswith('Resend:') \ and line != 'echo:Unknown command:""\n' \ and self.isOperational(): self._callback.on_comm_message(line) ##~~ Parsing for feedback commands if feedback_controls and feedback_matcher and not "_all" in feedback_errors: try: self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors) except: # something went wrong while feedback matching self._logger.exception("Error while trying to apply feedback control matching, disabling it") feedback_errors.append("_all") ##~~ Parsing for pause triggers if pause_triggers and not self.isStreaming(): if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) self.setPause(not self.isPaused()) ### Connection attempt elif self._state == self.STATE_CONNECTING: if "start" in line and not startSeen: startSeen = True self._sendCommand("M110") self._clear_to_send.set() elif "ok" in line: self._onConnected() elif time.time() > self._timeout: self.close() ### Operational elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: if "ok" in line: # if we still have commands to process, process them if self._resendDelta is not None: self._resendNextCommand() elif self._sendFromQueue(): pass # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): self._handleResendRequest(line) except Exception as ex: self._logger.exception("Something crashed inside the USB connection.") errorMsg = "See octoprint.log for details" self._log(ex.message) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) self._log("Connection closed, closing down monitor") def _statusProgressQueueCallback(self, status_obj): """ Auxiliar callback method to push the status object that comes from the printer into the queue :param status_obj: :return: """ # calls the Printer object to update the progress values self._callback.updateProgress(status_obj) self._callback.on_comm_progress() def _onConnected(self): """ Post connection callback """ # starts the connection monitor thread self._beeConn.startConnectionMonitor() self._temperature_timer = RepeatedTimer(self._timeout_intervals.get("temperature", 4.0), self._poll_temperature, run_first=True) self._temperature_timer.start() if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() payload = dict(port=self._port, baudrate=self._baudrate) eventManager().fire(Events.CONNECTED, payload) def _poll_temperature(self): """ Polls the temperature after the temperature timeout, re-enqueues itself. If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll will be done. """ try: if self.isOperational() and not self.isStreaming() and not self._long_running_command and not self._heating: self.sendCommand("M105", cmd_type="temperature_poll") except Exception as e: self._log("Error polling temperature %s" % str(e)) def getCommandsInterface(self): """ Returns the commands interface for BVC printers :return: """ return self._beeCommands def _connShutdownHook(self): """ Function to be called by the BVC driver to shutdown the connection :return: """ self._callback.on_comm_force_disconnect() def _preparePrintThread(self): """ Thread code that runs while the print job is being prepared :return: """ # waits for heating/file transfer while self._beeCommands.isTransferring(): time.sleep(1) self._changeState(self.STATE_HEATING) while self._beeCommands.isHeating(): time.sleep(1) self._changeState(self.STATE_PRINTING) # Starts the real printing operation self._currentFile.start() payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_STARTED, payload) # sends usage statistics self._sendUsageStatistics('start') # starts the progress status thread self.startPrintStatusProgressMonitor() if self._heatupWaitStartTime is not None: self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) self._heatupWaitStartTime = None self._heating = False def _sendUsageStatistics(self, operation): """ Calls and external executable to send usage statistics to a remote cloud server :param operation: Supports 'start' (Start Print), 'cancel' (Cancel Print), 'stop' (Print finished) operations :return: true in case the operation was successfull or false if not """ _logger = logging.getLogger() biExePath = settings().getBaseFolder('bi') + '/bi_azure' if operation != 'start' and operation != 'cancel' and operation != 'stop': return False if os.path.exists(biExePath) and os.path.isfile(biExePath): printerSN = self.getConnectedPrinterSN() if printerSN is None: _logger.error("Could not get Printer Serial Number for statistics communication.") return False else: cmd = '%s %s %s' % (biExePath,str(printerSN), str(operation)) _logger.info(u"Running %s" % cmd) import subprocess p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) (output, err) = p.communicate() p_status = p.wait() if p_status == 0 and 'IOTHUB_CLIENT_CONFIRMATION_OK' in output: _logger.info(u"Statistics sent to remote server. (Operation: %s)" % operation) return True else: _logger.info(u"Failed sending statistics to remote server. (Operation: %s)" % operation) return False
class EnclosurePlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.BlueprintPlugin, octoprint.plugin.EventHandlerPlugin): enclosureSetTemperature=0.0 enclosureCurrentTemperature=0.0 enclosureCurrentHumidity=0.0 def startGPIO(self): self.configureGPIO(self._settings.get_int(["heaterPin"])) self.configureGPIO(self._settings.get_int(["io1"])) self.configureGPIO(self._settings.get_int(["io2"])) self.configureGPIO(self._settings.get_int(["io3"])) self.configureGPIO(self._settings.get_int(["io4"])) def configureGPIO(self, pin): os.system("gpio -g mode "+str(pin)+" out") os.system("gpio -g write "+str(pin)+" 1") def startTimer(self): self._checkTempTimer = RepeatedTimer(10, self.checkEnclosureTemp, None, None, True) self._checkTempTimer.start() def toFloat(self, value): try: val = float(value) return val except: self._logger.info("Failed to convert to float") return 0 def checkEnclosureTemp(self): if self._settings.get(["dhtModel"]) == 1820 or self._settings.get(["dhtModel"]) == '1820': stdout = Popen("sudo "+self._settings.get(["getTempScript"])+" "+str(self._settings.get(["dhtModel"])), shell=True, stdout=PIPE).stdout else: stdout = Popen("sudo "+self._settings.get(["getTempScript"])+" "+str(self._settings.get(["dhtModel"]))+" "+str(self._settings.get(["dhtPin"])), shell=True, stdout=PIPE).stdout sTemp = stdout.read() sTemp.replace(" ", "") if sTemp.find("Failed") != -1: self._logger.info("Failed to read Temperature") else: #self._logger.info(sTemp) self.enclosureCurrentTemperature = self.toFloat(sTemp) #self._logger.info("enclosureCurrentTemperature is: %s",self.enclosureCurrentTemperature) if self._settings.get(["dhtModel"]) != '1820': stdout = Popen("sudo "+self._settings.get(["getHumiScript"])+" "+str(self._settings.get(["dhtModel"]))+" "+str(self._settings.get(["dhtPin"])), shell=True, stdout=PIPE).stdout sHum = stdout.read() sHum.replace(" ", "") if sHum.find("Failed") != -1: self._logger.info("Failed to read Humidity") else: self._logger.info(sHum) self.enclosureCurrentHumidity = self.toFloat(sHum) self._plugin_manager.send_plugin_message(self._identifier, dict(enclosuretemp=self.enclosureCurrentTemperature,enclosureHumidity=self.enclosureCurrentHumidity)) self.heaterHandler() def heaterHandler(self): command="" if self.enclosureCurrentTemperature<float(self.enclosureSetTemperature) and self._settings.get_boolean(["heaterEnable"]): os.system("gpio -g write "+str(self._settings.get_int(["heaterPin"]))+" 0") else: os.system("gpio -g write "+str(self._settings.get_int(["heaterPin"]))+" 1") os.system(command) #~~ StartupPlugin mixin def on_after_startup(self): self.startTimer() self.startGPIO() #~~ Blueprintplugin mixin @octoprint.plugin.BlueprintPlugin.route("/setEnclosureTemperature", methods=["GET"]) def setEnclosureTemperature(self): self.enclosureSetTemperature = flask.request.values["enclosureSetTemp"] self.heaterHandler() return flask.jsonify(enclosureSetTemperature=self.enclosureSetTemperature,enclosureCurrentTemperature=self.enclosureCurrentTemperature) @octoprint.plugin.BlueprintPlugin.route("/getEnclosureSetTemperature", methods=["GET"]) def getEnclosureSetTemperature(self): return str(self.enclosureSetTemperature) @octoprint.plugin.BlueprintPlugin.route("/getEnclosureTemperature", methods=["GET"]) def getEnclosureTemperature(self): return str(self.enclosureCurrentTemperature) @octoprint.plugin.BlueprintPlugin.route("/setIO", methods=["GET"]) def setIO(self): io = flask.request.values["pin"] value = flask.request.values["status"] if value == "on": os.system("gpio -g write "+str(self._settings.get_int([io]))+" 0") else: os.system("gpio -g write "+str(self._settings.get_int([io]))+" 1") return flask.jsonify(success=True) #~~ EventPlugin mixin def on_event(self, event, payload): if event != "PrintDone": return if self._settings.get(['heaterEnable']): self.enclosureSetTemperature = 0 #~~ SettingsPlugin mixin def on_settings_save(self, data): old_heaterPin = self._settings.get_int(["heaterPin"]) old_dhtPin = self._settings.get_int(["dhtPin"]) old_io1 = self._settings.get_int(["io1"]) old_io2 = self._settings.get_int(["io2"]) old_io3 = self._settings.get_int(["io3"]) old_io4 = self._settings.get_int(["io4"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_heaterPin = self._settings.get_int(["heaterPin"]) new_dhtPin = self._settings.get_int(["dhtPin"]) new_io1 = self._settings.get_int(["io1"]) new_io2 = self._settings.get_int(["io2"]) new_io3 = self._settings.get_int(["io3"]) new_io4 = self._settings.get_int(["io4"]) if new_heaterPin != old_heaterPin: self.configureGPIO(new_heaterPin) if old_dhtPin != new_dhtPin: self.configureGPIO(new_dhtPin) if old_io1 != new_io1: self.configureGPIO(new_io1) if old_io2 != new_io2: self.configureGPIO(new_io2) if old_io3 != new_io3: self.configureGPIO(new_io3) if old_io4 != new_io4: self.configureGPIO(new_io4) def get_settings_defaults(self): return dict( heaterEnable=False, heaterPin=18, io1=17, io2=18, io3=21, io4=22, dhtPin=4, dhtModel=22, io1Enable=False, io2Enable=False, io3Enable=False, io4Enable=False, io1Label="IO1", io2Label="IO2", io3Label="IO3", io4Label="IO4", getTempScript="~/.octoprint/plugins/OctoPrint-Enclosure/extras/GetTemperature.py", getHumiScript="~/.octoprint/plugins/OctoPrint-Enclosure/extras/GetHumidity.py" ) #~~ TemplatePlugin def get_template_configs(self): return [dict(type="settings", custom_bindings=False)] ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/enclosure.js"] ) ##~~ Softwareupdate hook def get_update_information(self): return dict( enclosure=dict( displayName="Enclosure Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Enclosure", current=self._plugin_version, # update method: pip pip="https://github.com/vitormhenrique/OctoPrint-Enclosure/archive/{target_version}.zip" ) )
class PowercontrolPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.OctoPrintPlugin): ##~~ Initialization def __init__(self): self._cooldownOneTimeoutValue = None self._cooldownOneTimer = None self._cooldownTwoTimeoutValue = None self._cooldownTwoTimer = None self._helperWaitTimeoutValue = 1 self._helperWaitTimer = None self._pwrOneTimeoutValue = None self._pwrOneTimer = None self._pwrTwoTimeoutValue = None self._pwrTwoTimer = None self.cooldownDelay = "" self.inOnePin = "" self.inTwoPin = "" self.isRaspi = False self.onOneMessage = "" self.offOneMessage = "" self.onTwoMessage = "" self.offTwoMessage = "" self.powerinfoActive = False self.pwrMessage = "" self.relayOneName = "" self.relayOneCooldownEnabled = False self.relayTwoName = "" self.relayTwoCooldownEnabled = False self.showPwrOneRelay = False self.showPwrTwoRelay = False self.warnOnPwrOffRelayOne = False self.warnOnPwrOffRelayTwo = False ##~~ StartupPlugin mixin def on_after_startup(self): if sys.platform == "linux2": with open('/proc/cpuinfo', 'r') as infile: cpuinfo = infile.read() # Search for the cpu info match = re.search('Hardware\s+:\s+(\w+)$', cpuinfo, flags=re.MULTILINE | re.IGNORECASE) if match is None: # The hardware is not a pi self.isRaspi = False elif match.group(1) == 'BCM2708': self._logger.debug("Pi 1") self.isRaspi = True elif match.group(1) == 'BCM2709': self._logger.debug("Pi 2") self.isRaspi = True elif match.group(1) == 'BCM2710': self._logger.debug("Pi 3") self.isRaspi = True self.cooldownDelay = int(self._settings.get(["cooldownDelay"])) self.inOnePin = int(self._settings.get(["inOnePin"])) self.inTwoPin = int(self._settings.get(["inTwoPin"])) self.onOneMessage = self._settings.get(["onOneMessage"]) self.offOneMessage = self._settings.get(["offOneMessage"]) self.onTwoMessage = self._settings.get(["onTwoMessage"]) self.offTwoMessage = self._settings.get(["offTwoMessage"]) self._settings.set(["powerinfoActive"], self.powerinfoActive) self.relayOneName = self._settings.get(["relayOneName"]) self.relayOneCooldownEnabled = self._settings.get(["relayOneCooldownEnabled"]) self.relayTwoName = self._settings.get(["relayTwoName"]) self.relayTwoCooldownEnabled = self._settings.get(["relayTwoCooldownEnabled"]) self.showPwrOneRelay = self._settings.get(["showPwrOneRelay"]) self.showPwrTwoRelay = self._settings.get(["showPwrTwoRelay"]) self.warnOnPwrOffRelayOne = self._settings.get(["warnOnPwrOffRelayOne"]) self.warnOnPwrOffRelayTwo = self._settings.get(["warnOnPwrOffRelayTwo"]) self.updatePlugin() self._helperWaitTimer = RepeatedTimer(1, self._helper_wait_task) self._helperWaitTimer.start() def _helper_wait_task(self): self._helperWaitTimeoutValue -= 1 if self._helperWaitTimeoutValue <= 0: self._helperWaitTimer.cancel() self._helperWaitTimer = None self.get_helpers() def get_helpers(self): # Try to find powerinfo plugin helpers = self._plugin_manager.get_helpers("powerinfo") self._logger.info("helpers %s" % helpers) if helpers and "inOnePin" in helpers: self.powerinfoActive = True self._settings.set(["powerinfoActive"], self.powerinfoActive) self._logger.debug("Using powerinfo as reference") self.inOnePin = helpers["inOnePin"] self.inTwoPin = helpers["inTwoPin"] self.relayOneName = helpers["relayOneName"] self.relayTwoName = helpers["relayTwoName"] self.showPwrOneRelay = helpers["showPwrOneRelay"] self.showPwrTwoRelay = helpers["showPwrTwoRelay"] if self.isRaspi and not self.powerinfoActive: self._logger.debug("Powercontrol: Initialize GPIO") # Set GPIO layout like pin-number GPIO.setmode(GPIO.BOARD) # Configure our GPIO outputs GPIO.setup(self.inOnePin, GPIO.OUT) GPIO.setup(self.inTwoPin, GPIO.OUT) # Setup the initial state to high(off) GPIO.output(self.inOnePin, GPIO.HIGH) GPIO.output(self.inTwoPin, GPIO.HIGH) self.updatePlugin() ##~~ OctoPrintPlugin hook def hook_m117(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if gcode and gcode == "M117": self._logger.info("Got message: {0}".format(cmd)) self.pwrMessage = cmd[5:] if self.pwrMessage == self.onOneMessage: GPIO.output(self.inOnePin, GPIO.LOW) elif self.pwrMessage == self.offOneMessage: if self.relayOneCooldownEnabled: self._cooldownOneTimeoutValue = self.cooldownDelay self._cooldownOneTimer = RepeatedTimer(1, self._cooldownOne_task) self._cooldownOneTimer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="cooldownOne", timeout_value=self._cooldownOneTimeoutValue)) else: GPIO.output(self.inOnePin, GPIO.HIGH) elif self.pwrMessage == self.onTwoMessage: GPIO.output(self.inTwoPin, GPIO.LOW) elif self.pwrMessage == self.offTwoMessage: if self.relayTwoCooldownEnabled: self._cooldownTwoTimeoutValue = self.cooldownDelay self._cooldownTwoTimer = RepeatedTimer(1, self._cooldownTwo_task) self._cooldownTwoTimer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="cooldownTwo", timeout_value=self._cooldownTwoTimeoutValue)) else: GPIO.output(self.inTwoPin, GPIO.HIGH) def _cooldownOne_task(self): self._cooldownOneTimeoutValue -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="cooldownOne", timeout_value=self._cooldownOneTimeoutValue)) if self._cooldownOneTimeoutValue <= 0: self._cooldownOneTimer.cancel() self._cooldownOneTimer = None GPIO.output(self.inOnePin, GPIO.HIGH) self.updatePlugin() def _cooldownTwo_task(self): self._cooldownTwoTimeoutValue -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="cooldownTwo", timeout_value=self._cooldownTwoTimeoutValue)) if self._cooldownTwoTimeoutValue <= 0: self._cooldownTwoTimer.cancel() self._cooldownTwoTimer = None GPIO.output(self.inTwoPin, GPIO.HIGH) self.updatePlugin() ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( cooldownDelay="120", inOnePin="11", inTwoPin="12", onOneMessage="Printer on", offOneMessage="Printer off", onTwoMessage="Light on", offTwoMessage="Light off", powerinfoActive=False, relayOneName="Printer", relayOneCooldownEnabled=True, relayTwoName="Light", relayTwoCooldownEnabled=False, showPwrOneRelay=True, showPwrTwoRelay=False, warnOnPwrOffRelayOne=True, warnOnPwrOffRelayTwo=True ) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.cooldownDelay = int(self._settings.get(["cooldownDelay"])) self.inOnePin = int(self._settings.get(["inOnePin"])) self.inTwoPin = int(self._settings.get(["inTwoPin"])) self.onOneMessage = self._settings.get(["onOneMessage"]) self.offOneMessage = self._settings.get(["offOneMessage"]) self.onTwoMessage = self._settings.get(["onTwoMessage"]) self.offTwoMessage = self._settings.get(["offTwoMessage"]) self.relayOneName = self._settings.get(["relayOneName"]) self.relayOneCooldownEnabled = self._settings.get(["relayOneCooldownEnabled"]) self.relayTwoName = self._settings.get(["relayTwoName"]) self.relayTwoCooldownEnabled = self._settings.get(["relayTwoCooldownEnabled"]) self.showPwrOneRelay = self._settings.get(["showPwrOneRelay"]) self.showPwrTwoRelay = self._settings.get(["showPwrTwoRelay"]) self.warnOnPwrOffRelayOne = self._settings.get(["warnOnPwrOffRelayOne"]) self.warnOnPwrOffRelayTwo = self._settings.get(["warnOnPwrOffRelayTwo"]) self.updatePlugin() self._helperWaitTimeoutValue = 1 self._helperWaitTimer = RepeatedTimer(1, self._helper_wait_task) self._helperWaitTimer.start() ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/powercontrol.js"] ) ##~~ TemplatePlugin mixin def get_template_configs(self): if self.isRaspi: return [ dict(type="sidebar", name="Powercontrol"), dict(type="settings", name="Powercontrol", custom_bindings=False) ] else: return [ ] ##~~ SimpleApiPlugin mixin def on_api_get(self, request): return flask.jsonify(dict( rOneName=self.relayOneName, rOneShow=self.showPwrOneRelay, rTwoName=self.relayTwoName, rTwoShow=self.showPwrTwoRelay )) def get_api_commands(self): return dict(pwrOnRelayOne=[], pwrOffRelayOne=[], pwrOnRelayTwo=[], pwrOffRelayTwo=[], cancelOne=[], cancelCooldownOne=[], cancelTwo=[], cancelCooldownTwo=[]) def on_api_command(self, command, data): if command == "pwrOnRelayOne": GPIO.output(self.inOnePin, GPIO.LOW) self.updatePlugin() elif command == "pwrOffRelayOne": if self.warnOnPwrOffRelayOne: self._pwrOneTimeoutValue = 10 self._pwrOneTimer = RepeatedTimer(1, self._timerOne_task) self._pwrOneTimer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeoutOne", timeout_value=self._pwrOneTimeoutValue)) else: GPIO.output(self.inOnePin, GPIO.HIGH) self.updatePlugin() elif command == "pwrOnRelayTwo": GPIO.output(self.inTwoPin, GPIO.LOW) self.updatePlugin() elif command == "pwrOffRelayTwo": if self.warnOnPwrOffRelayTwo: self._pwrTwoTimeoutValue = 10 self._pwrTwoTimer = RepeatedTimer(1, self._timerTwo_task) self._pwrTwoTimer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeoutTwo", timeout_value=self._pwrTwoTimeoutValue)) else: GPIO.output(self.inTwoPin, GPIO.HIGH) self.updatePlugin() elif command == "cancelOne": self._pwrOneTimer.cancel() self._logger.info("Cancelled power off relay 1.") elif command == "cancelCooldownOne": self._cooldownOneTimer.cancel() self._logger.info("Cancelled cooldown power off relay 1.") elif command == "cancelTwo": self._pwrTwoTimer.cancel() self._logger.info("Cancelled power off relay 2.") elif command == "cancelCooldownTwo": self._cooldownTwoTimer.cancel() self._logger.info("Cancelled cooldown power off relay 2.") def _timerOne_task(self): self._pwrOneTimeoutValue -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeoutOne", timeout_value=self._pwrOneTimeoutValue)) if self._pwrOneTimeoutValue <= 0: self._pwrOneTimer.cancel() self._pwrOneTimer = None GPIO.output(self.inOnePin, GPIO.HIGH) self.updatePlugin() def _timerTwo_task(self): self._pwrTwoTimeoutValue -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeoutTwo", timeout_value=self._pwrTwoTimeoutValue)) if self._pwrTwoTimeoutValue <= 0: self._pwrTwoTimer.cancel() self._pwrTwoTimer = None GPIO.output(self.inTwoPin, GPIO.HIGH) self.updatePlugin() def updatePlugin(self): # Send our status update self._plugin_manager.send_plugin_message(self._identifier, dict(rOneName=self.relayOneName, rOneShow=self.showPwrOneRelay, rTwoName=self.relayTwoName, rTwoShow=self.showPwrTwoRelay)) ##~~ Softwareupdate hook def get_update_information(self): return dict( powercontrol=dict( displayName="Powercontrol Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Powercontrol", current=self._plugin_version, # update method: pip pip="https://github.com/konvader/OctoPrint-Relaycontrol/archive/{target_version}.zip" ) )
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 OctopodPlugin( octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.ProgressPlugin): def __init__(self): super(OctopodPlugin, self).__init__() self._logger = logging.getLogger("octoprint.plugins.octopod") self._checkTempTimer = None self._ifttt_alerts = IFTTTAlerts(self._logger) self._job_notifications = JobNotifications(self._logger, self._ifttt_alerts) self._tool_notifications = ToolsNotifications(self._logger, self._ifttt_alerts) self._bed_notifications = BedNotifications(self._logger, self._ifttt_alerts) self._mmu_assitance = MMUAssistance(self._logger, self._ifttt_alerts) self._paused_for_user = PausedForUser(self._logger, self._ifttt_alerts) self._palette2 = Palette2Notifications(self._logger, self._ifttt_alerts) self._layerNotifications = LayerNotifications(self._logger, self._ifttt_alerts) self._check_soc_temp_timer = None self._soc_timer_interval = 5.0 if debug_soc_temp else 30.0 self._soc_temp_notifications = SocTempNotifications( self._logger, self._ifttt_alerts, self._soc_timer_interval, debug_soc_temp) # StartupPlugin mixin def on_after_startup(self): self._logger.info("OctoPod loaded!") # Set logging level to what we have in the settings if self._settings.get_boolean(["debug_logging"]): self._logger.setLevel(logging.DEBUG) else: self._logger.setLevel(logging.INFO) # Register to listen for messages from other plugins self._plugin_manager.register_message_receiver(self.on_plugin_message) # Start timer that will check bed temperature and send notifications if needed self._restart_timer() # if running on linux then check soc temperature if sys.platform.startswith("linux") or debug_soc_temp: sbc = RPi( self._logger) if debug_soc_temp else SBCFactory().factory( self._logger) if sbc.is_supported: self._soc_temp_notifications.sbc = sbc sbc.debugMode = debug_soc_temp self._soc_temp_notifications.send_plugin_message = self.send_plugin_message self.start_soc_timer(self._soc_timer_interval) # SettingsPlugin mixin def get_settings_defaults(self): return dict( debug_logging=False, server_url='http://octopodprint.com/', camera_snapshot_url='http://localhost:8080/?action=snapshot', tokens=[], temp_interval=5, tool0_low=0, bed_low=30, bed_target_temp_hold=10, mmu_interval=5, pause_interval=5, palette2_printing_error_codes=[103, 104, 111, 121], progress_type= '50', # 0=disabled, 25=every 25%, 50=every 50%, 100=only when finished ifttt_key='', ifttt_name='', soc_temp_high=75, webcam_flipH=False, webcam_flipV=False, webcam_rotate90=False) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._logger.setLevel(logging.DEBUG) else: self._logger.setLevel(logging.INFO) def get_settings_version(self): return 10 def on_settings_migrate(self, target, current): if current == 1: # add the 2 new values included self._settings.set(['temp_interval'], self.get_settings_defaults()["temp_interval"]) self._settings.set(['bed_low'], self.get_settings_defaults()["bed_low"]) if current <= 2: self._settings.set( ['bed_target_temp_hold'], self.get_settings_defaults()["bed_target_temp_hold"]) if current <= 3: self._settings.set(['mmu_interval'], self.get_settings_defaults()["mmu_interval"]) if current <= 4: self._settings.set(['pause_interval'], self.get_settings_defaults()["pause_interval"]) if current <= 5: self._settings.set(['tool0_low'], self.get_settings_defaults()["tool0_low"]) if current <= 6: self._settings.set( ['palette2_printing_error_codes'], self.get_settings_defaults()["palette2_printing_error_codes"]) if current <= 7: self._settings.set(['progress_type'], self.get_settings_defaults()["progress_type"]) if current <= 8: self._settings.set(['ifttt_key'], self.get_settings_defaults()["ifttt_key"]) self._settings.set(['ifttt_name'], self.get_settings_defaults()["ifttt_name"]) if current <= 9: self._settings.set(['soc_temp_high'], self.get_settings_defaults()["soc_temp_high"]) self._settings.set(['webcam_flipH'], self._settings.global_get(["webcam", "flipH"])) self._settings.set(['webcam_flipV'], self._settings.global_get(["webcam", "flipV"])) self._settings.set(['webcam_rotate90'], self._settings.global_get( ["webcam", "rotate90"])) # AssetPlugin mixin def get_assets(self): # Define your plugin's asset files to automatically include in the # core UI here. return dict( js=["js/octopod.js"], css=["css/octopod.css"], ) # ProgressPlugin # progress-hook def on_print_progress(self, storage, path, progress): # progress 0 - 100 self._job_notifications.on_print_progress(self._settings, progress) # EventHandlerPlugin mixin def on_event(self, event, payload): if event == Events.PRINTER_STATE_CHANGED: self._job_notifications.send__print_job_notification( self._settings, self._printer, payload) elif event == "DisplayLayerProgress_layerChanged": # Event sent from DisplayLayerProgress plugin when there was a detected layer changed self._layerNotifications.layer_changed(self._settings, payload["currentLayer"]) elif event == Events.PRINT_STARTED or event == Events.PRINT_DONE or event == Events.PRINT_CANCELLED \ or event == Events.PRINT_FAILED: # Reset layers for which we need to send a notification. Each new print job has its own self._layerNotifications.reset_layers() # SimpleApiPlugin mixin def update_token(self, old_token, new_token, device_name, printer_id, printer_name, language_code): self._logger.debug("Received tokens for %s." % device_name) existing_tokens = self._settings.get(["tokens"]) # Safety check in case a user manually modified config.yaml and left invalid JSON if existing_tokens is None: existing_tokens = [] found = False updated = False for token in existing_tokens: # Check if existing token has been updated if token["apnsToken"] == old_token and token[ "printerID"] == printer_id: if old_token != new_token: self._logger.debug("Updating token for %s." % device_name) # Token that exists needs to be updated with new token token["apnsToken"] = new_token token["date"] = datetime.datetime.now().strftime("%x %X") updated = True found = True elif token["apnsToken"] == new_token and token[ "printerID"] == printer_id: found = True if found: if printer_name is not None and ( "printerName" not in token or token["printerName"] != printer_name): # Printer name in OctoPod has been updated token["printerName"] = printer_name token["date"] = datetime.datetime.now().strftime("%x %X") updated = True if language_code is not None and ( "languageCode" not in token or token["languageCode"] != language_code): # Language being used by OctoPod has been updated token["languageCode"] = language_code token["date"] = datetime.datetime.now().strftime("%x %X") updated = True break if not found: self._logger.debug("Adding token for %s." % device_name) # Token was not found so we need to add it existing_tokens.append({ 'apnsToken': new_token, 'deviceName': device_name, 'date': datetime.datetime.now().strftime("%x %X"), 'printerID': printer_id, 'printerName': printer_name, 'languageCode': language_code }) updated = True if updated: # Save new settings self._settings.set(["tokens"], existing_tokens) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) self._logger.debug("Tokens saved") def get_api_commands(self): return dict( updateToken=["oldToken", "newToken", "deviceName", "printerID"], test=[], snooze=["eventCode", "minutes"], addLayer=["layer"], removeLayer=["layer"], getLayers=[], getSoCTemps=[]) def on_api_command(self, command, data): if not user_permission.can(): return flask.make_response("Insufficient rights", 403) if command == 'updateToken': # Convert from ASCII to UTF-8 since some chars will fail otherwise (e.g. apostrophe) - Only for Python 2 if sys.version_info[0] == 2: data["deviceName"] = data["deviceName"].encode("utf-8") printer_name = data[ "printerName"] if 'printerName' in data else None language_code = data[ "languageCode"] if 'languageCode' in data else None self.update_token("{oldToken}".format(**data), "{newToken}".format(**data), "{deviceName}".format(**data), "{printerID}".format(**data), printer_name, language_code) elif command == 'test': payload = dict(state_id="OPERATIONAL", state_string="Operational") code = self._job_notifications.send__print_job_notification( self._settings, self._printer, payload, data["server_url"], data["camera_snapshot_url"], data["camera_flip_h"], data["camera_flip_v"], data["camera_rotate90"], True) return flask.jsonify(dict(code=code)) elif command == 'snooze': if data["eventCode"] == 'mmu-event': self._mmu_assitance.snooze(data["minutes"]) else: return flask.make_response("Snooze for unknown event", 400) elif command == 'addLayer': self._layerNotifications.add_layer(data["layer"]) elif command == 'removeLayer': self._layerNotifications.remove_layer(data["layer"]) elif command == 'getLayers': return flask.jsonify( dict(layers=self._layerNotifications.get_layers())) elif command == 'getSoCTemps': return flask.jsonify(self._soc_temp_notifications.get_soc_temps()) else: return flask.make_response("Unknown command", 400) # TemplatePlugin mixin def get_template_configs(self): return [ dict(type="settings", name="OctoPod Notifications", custom_bindings=True) ] # 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(octopod=dict( displayName="OctoPod Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-OctoPod", current=self._plugin_version, # update method: pip pip= "https://github.com/gdombiak/OctoPrint-OctoPod/archive/{target_version}.zip" )) # Plugin messages def on_plugin_message(self, plugin, data, permissions=None): self._palette2.check_plugin_message(self._settings, plugin, data) def send_plugin_message(self, data): self._plugin_manager.send_plugin_message(self._identifier, data) # Timer functions def _restart_timer(self): # stop the timer if self._checkTempTimer: self._logger.debug(u"Stopping Timer...") self._checkTempTimer.cancel() self._checkTempTimer = None # start a new timer interval = self._settings.get_int(['temp_interval']) if interval: self._logger.debug(u"Starting Timer...") self._checkTempTimer = RepeatedTimer(interval, self.run_timer_job, None, None, True) self._checkTempTimer.start() def run_timer_job(self): self._bed_notifications.check_temps(self._settings, self._printer) self._tool_notifications.check_temps(self._settings, self._printer) def start_soc_timer(self, interval): self._logger.debug(u"Monitoring SoC temp with Timer") self._check_soc_temp_timer = RepeatedTimer(interval, self.update_soc_temp, run_first=True) self._check_soc_temp_timer.start() def update_soc_temp(self): self._soc_temp_notifications.check_soc_temp(self._settings) # GCODE hook def process_gcode(self, comm, line, *args, **kwargs): line = self._paused_for_user.process_gcode(self._settings, self._printer, line) return self._mmu_assitance.process_gcode(self._settings, line)
class Timer(): def __init__(self, timeout_minutes): self._logEnabled = False self._timerEnabled = False self._logger = None self._timer = None self._plugin_manager = None self._identifier = None self._timeout_seconds = None self._cb = None self._default_timeout_seconds = self._min2sec(timeout_minutes) def initialize(self, plugin_manager, identifier, cb): if self._logEnabled is False: self._logEnabled = True self._logger = logging.getLogger("octoprint.plugins.{}".format(__name__)) self._plugin_manager = plugin_manager self._identifier = identifier self._cb = cb def initializeTimer(self): self._timer = RepeatedTimer(1, self._timer_task) def start(self): if self._timerEnabled is False: if self._timer is None: self.initializeTimer() self._timeout_seconds = self._default_timeout_seconds self._timerEnabled = True self._timer.start() self._logger.info("Powersave timer started with: {} mintues".format(self._sec2min(self._default_timeout_seconds))) def cancel(self): if self._timerEnabled is True: self._timerEnabled = False if self._timer is not None: self._plugin_manager.send_plugin_message(self._identifier, dict(type="cancel")) self._timer.cancel() self._timer = None self._logger.info("Powersave timer canceled") def setNewTimeoutMinutes(self, minutes): if not self._min2sec(minutes) == self._default_timeout_seconds: self._default_timeout_seconds = self._min2sec(minutes) self.cancel() self.start() self._logger.info("Powersave timeout value updated to: {}".format(minutes)) def reset(self): self._timeout_value = self.default_timeout_seconds self._logger.info("Powersave timer reset") def _timer_task(self): self._timeout_seconds -= 1 if self._timeout_seconds < 60 * 5: self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeout", timeout_value=self._timeout_seconds)) if self._timeout_seconds <= 0: self._timer.cancel() self._timer = None self._cb() def _sec2min(self, seconds): return seconds / 60 def _min2sec(self, minutes): return 60 * minutes
class MachineCom(octoprint.plugin.MachineComPlugin): #pylint: disable=too-many-instance-attributes, too-many-public-methods _logger = None _serialLogger = None _state = None _port = None _baudrate = None _printer_uri = None _print_job_uri = None _authentise_process = None _authentise_model = None _authentise_url = None _session = None _command_uri_queue = None _printer_status_timer = None _tool_tempuratures = None _bed_tempurature = None _print_progress = None _callback = None _printer_profile_manager = None monitoring_thread = None _monitoring_active = False _errorValue = None def __init__(self): self._logger = logging.getLogger(__name__) self._serialLogger = logging.getLogger("SERIAL") self._command_uri_queue = comm_helpers.TypedQueue() self._state = self.STATE_NONE def startup(self, callbackObject=None, printerProfileManager=None): if callbackObject == None: callbackObject = comm_helpers.MachineComPrintCallback() self._callback = callbackObject self._printer_profile_manager = printerProfileManager self._authentise_url = self._settings.get(['authentise_url']) #pylint: disable=no-member def connect(self, port=None, baudrate=None): try: self._session = helpers.session(self._settings) #pylint: disable=no-member helpers.claim_node(self.node_uuid, self._settings, self._logger) #pylint: disable=no-member except (helpers.ClaimNodeException, helpers.SessionException) as e: self._errorValue = e.message self._change_state(PRINTER_STATE['ERROR']) return if port == None: port = settings().get(["serial", "port"]) if baudrate == None: settings_baudrate = settings().getInt(["serial", "baudrate"]) if settings_baudrate is None: baudrate = 0 else: baudrate = settings_baudrate self._port = port self._baudrate = baudrate self._printer_uri = self._get_or_create_printer(port, baudrate) self._authentise_process = helpers.run_client(self._settings) #pylint: disable=no-member # monitoring thread self._monitoring_active = True self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitor") self.monitoring_thread.daemon = True self.monitoring_thread.start() self._printer_status_timer = RepeatedTimer( lambda: comm_helpers.get_interval("temperature", default_value=10.0), self._update_printer_data, run_first=True ) self._printer_status_timer.start() self._change_state(PRINTER_STATE['CONNECTING']) def _get_or_create_printer(self, port, baud_rate): client_url = urlparse.urljoin(self._authentise_url, '/client/{}/'.format(self.node_uuid)) #pylint: disable=no-member url = urlparse.urljoin(self._authentise_url, '/printer/instance/?filter[client]={}'.format(quote_plus(client_url))) target_printer = None self._log('Getting printer list from: {}'.format(url)) printer_get_resp = self._session.get(url=url) for printer in printer_get_resp.json()["resources"]: if printer['port'] == port: target_printer = printer self._log('Printer {} matches selected port {}'.format(printer, port)) break if target_printer: if target_printer['baud_rate'] != baud_rate: self._session.put(target_printer["uri"], json={'baud_rate': baud_rate}) return target_printer['uri'] else: self._log('No printer found for port {}. Creating it.'.format(port)) if os.path.exists('/usr/lib/python2.7/dist-packages/octoprint/static/img/type_a_machines.svg'): model = 14 else: model = 9 payload = { 'baud_rate' : baud_rate, 'client' : client_url, 'name' : 'Octoprint Printer', 'port' : port, 'printer_model' : 'https://print.dev-auth.com/printer/model/{}/'.format(model), } create_printer_resp = self._session.post(urlparse.urljoin(self._authentise_url, '/printer/instance/'), json=payload) return create_printer_resp.headers["Location"] # #~~ internal state management def _change_state(self, new_state): # Change the printer state if self._state == new_state: return old_state = self._state old_state_string = self.getStateString() self._state = new_state self._log("Changed printer state from '{}' to '{}'".format(old_state_string, self.getStateString())) self._callback.on_comm_state_change(new_state) # Deal with firing nessesary events if new_state in [PRINTER_STATE['OPERATIONAL'], PRINTER_STATE['PRINTING'], PRINTER_STATE['PAUSED']]: # Send connected event if needed if old_state == PRINTER_STATE['CONNECTING']: payload = dict(port=self._port, baudrate=self._baudrate) eventManager().fire(Events.CONNECTED, payload) # Pausing and resuming printing if new_state == PRINTER_STATE['PAUSED']: eventManager().fire(Events.PRINT_PAUSED, None) elif new_state == PRINTER_STATE['PRINTING'] and old_state == PRINTER_STATE['PAUSED']: eventManager().fire(Events.PRINT_RESUMED, None) # New print elif new_state == PRINTER_STATE['PRINTING']: eventManager().fire(Events.PRINT_STARTED, None) self._callback.on_comm_set_job_data('Authentise Streaming Print', 10000, None) # It is not easy to tell the difference between an completed print and a cancled print at this point elif new_state == PRINTER_STATE['OPERATIONAL'] and old_state != PRINTER_STATE['CONNECTING']: eventManager().fire(Events.PRINT_DONE, None) self._callback.on_comm_set_job_data(None, None, None) elif new_state == PRINTER_STATE['CLOSED']: eventManager().fire(Events.DISCONNECTED) elif new_state in [PRINTER_STATE['ERROR'], PRINTER_STATE['CLOSED_WITH_ERROR']]: eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def _log(self, message): self._callback.on_comm_log(message) self._serialLogger.debug(message) ##~~ getters def getState(self): return self._state def getStateId(self, state=None): if state is None: state = self._state possible_states = filter(lambda x: x.startswith("STATE_"), self.__class__.__dict__.keys()) #pylint: disable=bad-builtin, deprecated-lambda for possible_state in possible_states: if getattr(self, possible_state) == state: return possible_state[len("STATE_"):] return "UNKNOWN" def getStateString(self, state=None): #pylint: disable=arguments-differ if not state: state = self._state if state in [PRINTER_STATE['ERROR'], PRINTER_STATE['CLOSED_WITH_ERROR']]: return "Error: {}".format(self.getErrorString()) return PRINTER_STATE_REVERSE[state].title() def getErrorString(self): return self._errorValue def isClosedOrError(self): return self._state in [PRINTER_STATE['ERROR'], PRINTER_STATE['CLOSED_WITH_ERROR'], PRINTER_STATE['CLOSED']] def isError(self): return self._state in [PRINTER_STATE['ERROR'], PRINTER_STATE['CLOSED_WITH_ERROR']] def isOperational(self): return self._state in [ PRINTER_STATE['OPERATIONAL'], PRINTER_STATE['PRINTING'], PRINTER_STATE['PAUSED']] def isPrinting(self): return self._state == PRINTER_STATE['PRINTING'] def isStreaming(self): return False def isPaused(self): return self._state == PRINTER_STATE['PAUSED'] def isBusy(self): return self.isPrinting() or self.isPaused() def isSdReady(self): return False def isSdFileSelected(self): return False def isSdPrinting(self): return False def getSdFiles(self): return def getPrintProgress(self): return self._print_progress['percent_complete'] if self._print_progress else None def getPrintFilepos(self): return int(self._print_progress['percent_complete']*10000) if self._print_progress else None def getPrintTime(self): return self._print_progress['elapsed'] if self._print_progress else None def getCleanedPrintTime(self): return self._print_progress['elapsed'] if self._print_progress else None def getTemp(self): return self._tool_tempuratures def getBedTemp(self): return self._bed_tempurature def getOffsets(self): return {} def getCurrentTool(self): return 0 def getConnection(self): return self._port, self._baudrate def getTransport(self): return ##~~ external interface def close(self, is_error=False, wait=True, *args, **kwargs): #pylint: disable=unused-argument if self._printer_status_timer: self._printer_status_timer.cancel() self._monitoring_active = False printing = self.isPrinting() or self.isPaused() if printing: eventManager().fire(Events.PRINT_FAILED, None) # close the Authentise client if it is open if self._authentise_process: self._authentise_process.send_signal(2) #send the SIGINT signal self._print_job_uri = None self._change_state(PRINTER_STATE['CLOSED']) def setTemperatureOffset(self, offsets): pass def fakeOk(self): pass def sendCommand(self, cmd, cmd_type=None, processed=False, force=False): #pylint: disable=unused-argument, arguments-differ cmd = cmd.encode('ascii', 'replace') if not processed: cmd = comm_helpers.process_gcode_line(cmd) if not cmd: return if self.isOperational(): data = {'command': cmd} printer_command_url = urlparse.urljoin(self._printer_uri, 'command/') response = self._session.post(printer_command_url, json=data) if not response.ok: self._log( 'Warning: Got invalid response {}: {} for {}: {}'.format( response.status_code, response.content, response.request.url, response.request.body)) return self._log( 'Sent {} to {} with response {}: {}'.format( response.request.body, response.request.url, response.status_code, response.content)) command_uri = response.headers['Location'] self._command_uri_queue.put({ 'uri' : command_uri, 'start_time' : time.time(), 'previous_time' : None, }) def startPrint(self): pass def selectFile(self, model_uri, sd): pass def unselectFile(self): pass def _send_pause_cancel_request(self, status): try: response = self._session.put(self._print_job_uri, json={'status':status}) except (requests.exceptions.MissingSchema, requests.exceptions.ConnectionError) as e: self._log('Request to {} generated error: {}'.format(self._print_job_uri, e)) response = None if response and response.ok: status_map = { 'cancel': PRINTER_STATE['OPERATIONAL'], 'pause': PRINTER_STATE['PAUSED'], 'resume': PRINTER_STATE['PRINTING'], } self._change_state(status_map[status]) def cancelPrint(self): if not self.isPrinting() and not self.isPaused(): return self._send_pause_cancel_request('cancel') def setPause(self, pause): if not pause and self.isPaused(): self._send_pause_cancel_request('resume') elif pause and self.isPrinting(): self._send_pause_cancel_request('pause') def sendGcodeScript(self, scriptName, replacements=None): return def startFileTransfer(self, filename, localFilename, remoteFilename): return def startSdFileTransfer(self, filename): return def endSdFileTransfer(self, filename): return def deleteSdFile(self, filename): return def refreshSdFiles(self): return def initSdCard(self): return def releaseSdCard(self): return def sayHello(self): #pylint: disable=no-self-use return def resetLineNumbers(self, number=0): #pylint: disable=unused-argument, no-self-use return ##~~ Serial monitor processing received messages def _readline(self): def _put_command_on_queue(data, start_time_diff): if start_time_diff < 120: self._command_uri_queue.put(data) current_time = time.time() try: command = self._command_uri_queue.get_nowait() except Queue.Empty: return '' start_time = command['start_time'] previous_time = command['previous_time'] command_uri = command['uri'] start_time_diff = current_time - start_time previous_time_diff = (current_time - previous_time) if previous_time else start_time_diff if previous_time_diff < 2: _put_command_on_queue({ 'uri' : command_uri, 'start_time' : start_time, 'previous_time' : previous_time, }, start_time_diff) return '' response = self._session.get(command_uri) if response.ok and response.json()['status'] in ['error', 'printer_offline']: return '' elif not response.ok or response.json()['status'] != 'ok': _put_command_on_queue({ 'uri' : command_uri, 'start_time' : start_time, 'previous_time' : current_time, }, start_time_diff) return '' command_response = response.json() self._log('Got response: {}, for command: {}'.format(command_response['response'], command_response['command'])) return command_response['response'] def _monitor_loop(self): self._log("Connected, starting monitor") while self._monitoring_active: try: line = self._readline() if not line: continue temps = parse_temps(line) if temps: tool_temps = {i: [temp['actual'], temp['target']] for i, temp in enumerate(temps['tools'])} bed_temp = (temps['bed']['actual'], temps['bed']['target']) if temps['bed'] else None self._callback.on_comm_temperature_update(tool_temps, bed_temp) self._callback.on_comm_message(line) except: #pylint: disable=bare-except self._logger.exception("Something crashed inside the serial connection loop," " please report this in OctoPrint's bug tracker:") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._change_state(PRINTER_STATE['ERROR']) time.sleep(0.1) self._log("Connection closed, closing down monitor") def _update_printer_data(self): if not self._printer_uri: return response = self._session.get(self._printer_uri) if not response.ok: self._log('Unable to get printer status: {}: {}'.format(response.status_code, response.content)) return response_data = response.json() if response_data['current_print'] and response_data['current_print']['status'].lower() != 'new': self._print_job_uri = response_data['current_print']['job_uri'] else: self._print_job_uri = None self._update_state(response_data) self._update_temps(response_data) self._update_progress(response_data) def _update_temps(self, response_data): temps = response_data['temperatures'] self._tool_tempuratures = {0: [ temps['extruder1'].get('current') if temps.get('extruder1') else None, temps['extruder1'].get('target') if temps.get('extruder1') else None, ]} self._bed_tempurature = [ temps['bed'].get('current') if temps.get('bed') else None, temps['bed'].get('target') if temps.get('bed') else None, ] if temps.get('bed') else None self._callback.on_comm_temperature_update(self._tool_tempuratures, self._bed_tempurature) def _update_progress(self, response_data): current_print = response_data['current_print'] if current_print and response_data['current_print']['status'].lower() != 'new': self._print_progress = { 'percent_complete' : current_print['percent_complete']/100, 'elapsed' : current_print['elapsed'], 'remaining' : current_print['remaining'], } self._callback.on_comm_set_progress_data( current_print['percent_complete'], current_print['percent_complete']*100 if current_print['percent_complete'] else None, current_print['elapsed'], current_print['remaining'], ) else: self._print_progress = None self._callback.on_comm_set_progress_data(None, None, None, None) def _update_state(self, response_data): if response_data['status'].lower() == 'online': if not response_data['current_print'] or response_data['current_print']['status'].lower() == 'new': self._change_state(PRINTER_STATE['OPERATIONAL']) elif response_data['current_print']: if response_data['current_print']['status'].lower() in ['printing', 'warming_up']: self._change_state(PRINTER_STATE['PRINTING']) elif response_data['current_print']['status'].lower() == 'paused': self._change_state(PRINTER_STATE['PAUSED']) else: self._log('Unknown print state: {}'.format(response_data['current_print']['status'])) else: self._change_state(PRINTER_STATE['CONNECTING'])
class NavBarPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin): def __init__(self): self.isRaspi = False self.debugMode = False # to simulate temp on Win/Mac self.displayRaspiTemp = True self._checkTempTimer = None def on_after_startup(self): self.displayRaspiTemp = self._settings.get(["displayRaspiTemp"]) self._logger.debug("displayRaspiTemp: %s" % self.displayRaspiTemp) if sys.platform == "linux2": with open('/proc/cpuinfo', 'r') as infile: cpuinfo = infile.read() # Match a line like 'Hardware : BCM2709' match = re.search('^Hardware\s+:\s+(\w+)$', cpuinfo, flags=re.MULTILINE | re.IGNORECASE) if match is None: # Couldn't find the hardware, assume it isn't a pi. self.isRaspi = False elif match.group(1) == 'BCM2708': self._logger.debug("Pi 1") self.isRaspi = True elif match.group(1) == 'BCM2709': self._logger.debug("Pi 2") self.isRaspi = True if self.isRaspi and self.displayRaspiTemp: self._logger.debug("Let's start RepeatedTimer!") self.startTimer(30.0) elif self.debugMode: self.isRaspi = True if self.displayRaspiTemp: self.startTimer(5.0) self._logger.debug("is Raspberry Pi? - %s" % self.isRaspi) def startTimer(self, interval): self._checkTempTimer = RepeatedTimer(interval, self.checkRaspiTemp, None, None, True) self._checkTempTimer.start() def checkRaspiTemp(self): from sarge import run, Capture self._logger.debug("Checking Raspberry Pi internal temperature") if sys.platform == "linux2": p = run("/opt/vc/bin/vcgencmd measure_temp", stdout=Capture()) p = p.stdout.text elif self.debugMode: import random def randrange_float(start, stop, step): return random.randint(0, int((stop - start) / step)) * step + start p = "temp=%s'C" % randrange_float(5, 60, 0.1) self._logger.debug("response from sarge: %s" % p) match = re.search('=(.*)\'', p) if not match: self.isRaspi = False else: temp = match.group(1) self._logger.debug("match: %s" % temp) self._plugin_manager.send_plugin_message(self._identifier, dict(israspi=self.isRaspi, raspitemp=temp)) ##~~ SettingsPlugin def get_settings_defaults(self): return dict(displayRaspiTemp = self.displayRaspiTemp) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.displayRaspiTemp = self._settings.get(["displayRaspiTemp"]) if self.displayRaspiTemp: interval = 5.0 if self.debugMode else 30.0 self.startTimer(interval) else: if self._checkTempTimer is not None: try: self._checkTempTimer.cancel() except: pass self._plugin_manager.send_plugin_message(self._identifier, dict()) ##~~ TemplatePlugin API def get_template_configs(self): if self.isRaspi: return [ dict(type="settings", template="navbartemp_settings_raspi.jinja2") ] else: return [] ##~~ AssetPlugin API def get_assets(self): return { "js": ["js/navbartemp.js"], "css": ["css/navbartemp.css"], "less": ["less/navbartemp.less"] } ##~~ Softwareupdate hook def get_update_information(self): return dict( navbartemp=dict( displayName="Navbar Temperature Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-NavbarTemp", current=self._plugin_version, # update method: pip w/ dependency links pip="https://github.com/imrahil/OctoPrint-NavbarTemp/archive/{target_version}.zip" ) )
class FirmwareUpdatePlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): self.isUpdating = False self._checkTimer = None self.updatePID = None def get_assets(self): return { "js": ["js/firmwareupdate.js"] } def startTimer(self, interval): self._checkTimer = RepeatedTimer(interval, self.checkStatus, run_first=True, condition=self.checkStatus) self._checkTimer.start() def checkStatus(self): update_result = open('/home/pi/Marlin/.build_log').read() if 'No device matching following was found' in update_result: self._logger.info("Failed update...") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed", reason="A connected device was not found.")) return False elif 'FAILED' in update_result: self._logger.info("Failed update...") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed")) return False elif 'bytes of flash verified' in update_result and 'successfully' in update_result : self._logger.info("Successful update!") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="completed")) return False elif 'ReceiveMessage(): timeout' in update_result: self._logger.info("Update timed out. Check if port is already in use!") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed", reason="Device timed out. Please check that the port is not in use!")) p = psutil.Process(self.updatePID) for child in p.children(recursive=True): child.kill() p.kill() return False elif 'error:' in update_result: error_list = [] with open('/home/pi/Marlin/.build_log') as myFile: for num, line in enumerate(myFile, 1): if 'error:' in line: error_list.append(line) compileError = '<pre>' + ''.join(error_list) + '</pre>' self._logger.info("Update failed. Compiling error.") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed", reason=compileError)) return False elif 'Make failed' in update_result: self._logger.info("Update failed. Compiling error.") self.isUpdating = False self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed", reason="Build failed.")) return False else: self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="continue")) return True def get_update_information(self): return dict( firmwareupdate_plugin=dict( displayName="FirmwareUpdate Plugin", displayVersion=self._plugin_version, type="github_commit", user="******", repo="OctoPrint-FirmwareUpdate", current=self._plugin_version, pip="https://github.com/Voxel8/OctoPrint-FirmwareUpdate/archive/{target_version}.zip" ) ) def get_api_commands(self): return dict( update_firmware=[], check_is_updating=[] ) def on_api_command(self, command, data): if command == "update_firmware": if not os.path.isdir("/home/pi/Marlin/"): self._logger.info("Firmware repository does not exist. Update cancelled.") self.isUpdating = False self._logger.info("Setting isUpdating to " + str(self.isUpdating)) self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, status="failed", reason="Firmware repository does not exist. Please clone before running update function.")) else: try: os.remove('/home/pi/Marlin/.build_log') except OSError: pass f = open("/home/pi/Marlin/.build_log", "w") self._logger.info("Firmware update request has been made. Running...") pro = Popen("cd /home/pi/Marlin; git fetch; git reset --hard origin/master; ./build.sh", stdout=f, stderr=f, shell=True, preexec_fn=os.setsid) self.updatePID = pro.pid self.isUpdating = True self._logger.info("Setting isUpdating to " + str(self.isUpdating)) self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, createPopup="yes")) self.startTimer(1.0) elif command == "check_is_updating": if self.isUpdating == True: self._logger.info("Setting isUpdating to " + str(self.isUpdating)) self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, createPopup="yes")) else: self._logger.info("Setting isUpdating to " + str(self.isUpdating)) self._plugin_manager.send_plugin_message(self._identifier, dict(isupdating=self.isUpdating, deletePopup="yes")) else: self._logger.info("Uknown command.") def on_api_get(self, request): return flask.make_response("Not found", 404) def get_template_configs(self): return [ dict(type="settings", name="Firmware Update", data_bind="visible: loginState.isAdmin()"), ]
class AutomaticshutdownPlugin(octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin): def __init__(self): self._automatic_shutdown_enabled = False self._timeout_value = None self._timer = None def get_assets(self): return dict(js=["js/automaticshutdown.js"]) def get_template_configs(self): return [dict(type="sidebar", name="Automatic Shutdown", custom_bindings=False, icon="power-off")] def get_api_commands(self): return dict(enable=[], disable=[], abort=[]) def on_api_command(self, command, data): import flask if command == "enable": self._automatic_shutdown_enabled = True elif command == "disable": self._automatic_shutdown_enabled = False elif command == "abort": self._timer.cancel() self._logger.info("Shutdown aborted.") def on_event(self, event, payload): if event != "SlicingDone": return if not self._automatic_shutdown_enabled or not self._settings.global_get(["server", "commands", "systemShutdownCommand"]): return if self._timer is not None: return self._timeout_value = 10 self._timer = RepeatedTimer(1, self._timer_task) self._timer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeout", timeout_value=self._timeout_value)) def _timer_task(self): self._timeout_value -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: self._timer.cancel() self._timer = None self._shutdown_system() def _shutdown_system(self): shutdown_command = self._settings.global_get(["server", "commands", "systemShutdownCommand"]) self._logger.info("Shutting down system with command: {command}".format(command=shutdown_command)) try: import sarge p = sarge.run(shutdown_command, async=True) except Exception as e: self._logger.exception("Error when shutting down: {error}".format(error=e)) return
def on_after_startup(self): self._logger.info("started Filament Box Healthcheck Plugin: ") t = RepeatedTimer(60, self.check_resources, run_first=True) t.start()
class NavBarPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin): def __init__(self): self.isRaspi = False self.debugMode = False # to simulate temp on Win/Mac self.displayRaspiTemp = True self._checkTempTimer = None def on_after_startup(self): self.displayRaspiTemp = self._settings.get(["displayRaspiTemp"]) self._logger.debug("displayRaspiTemp: %s" % self.displayRaspiTemp) if sys.platform == "linux2": with open('/proc/cpuinfo', 'r') as infile: cpuinfo = infile.read() # Match a line like 'Hardware : BCM2709' match = re.search('^Hardware\s+:\s+(\w+)$', cpuinfo, flags=re.MULTILINE | re.IGNORECASE) if match is None: # Couldn't find the hardware, assume it isn't a pi. self.isRaspi = False elif match.group(1) == 'BCM2708': self._logger.debug("Pi 1") self.isRaspi = True elif match.group(1) == 'BCM2709': self._logger.debug("Pi 2") self.isRaspi = True elif match.group(1) == 'BCM2835': self._logger.debug("Pi 3") self.isRaspi = True if self.isRaspi and self.displayRaspiTemp: self._logger.debug("Let's start RepeatedTimer!") self.startTimer(30.0) elif self.debugMode: self.isRaspi = True if self.displayRaspiTemp: self.startTimer(5.0) self._logger.debug("is Raspberry Pi? - %s" % self.isRaspi) def startTimer(self, interval): self._checkTempTimer = RepeatedTimer(interval, self.checkRaspiTemp, None, None, True) self._checkTempTimer.start() def checkRaspiTemp(self): from sarge import run, Capture self._logger.debug("Checking Raspberry Pi internal temperature") if sys.platform == "linux2": p = run("/opt/vc/bin/vcgencmd measure_temp", stdout=Capture()) p = p.stdout.text elif self.debugMode: import random def randrange_float(start, stop, step): return random.randint(0, int((stop - start) / step)) * step + start p = "temp=%s'C" % randrange_float(5, 60, 0.1) self._logger.debug("response from sarge: %s" % p) match = re.search('=(.*)\'', p) if not match: self.isRaspi = False else: temp = match.group(1) self._logger.debug("match: %s" % temp) self._plugin_manager.send_plugin_message(self._identifier, dict(israspi=self.isRaspi, raspitemp=temp)) ##~~ SettingsPlugin def get_settings_defaults(self): return dict(displayRaspiTemp = self.displayRaspiTemp) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.displayRaspiTemp = self._settings.get(["displayRaspiTemp"]) if self.displayRaspiTemp: interval = 5.0 if self.debugMode else 30.0 self.startTimer(interval) else: if self._checkTempTimer is not None: try: self._checkTempTimer.cancel() except: pass self._plugin_manager.send_plugin_message(self._identifier, dict()) ##~~ TemplatePlugin API def get_template_configs(self): if self.isRaspi: return [ dict(type="settings", template="navbartemp_settings_raspi.jinja2") ] else: return [] ##~~ AssetPlugin API def get_assets(self): return { "js": ["js/navbartemp.js"], "css": ["css/navbartemp.css"], "less": ["less/navbartemp.less"] } ##~~ Softwareupdate hook def get_update_information(self): return dict( navbartemp=dict( displayName="Navbar Temperature Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-NavbarTemp", current=self._plugin_version, # update method: pip w/ dependency links pip="https://github.com/imrahil/OctoPrint-NavbarTemp/archive/{target_version}.zip" ) )
class SnapPlugin(octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin): ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( interval=0, iam_access_key_id="", iam_secret_access_key="", s3_bucket_name="", webhook_url="", ) def on_after_startup(self): self._logger.info("Oh Snap! (Current Interval: %s)", self._settings.get(["interval"])) # TODO Refactor if possible these event calls into meta invocations similar to `.send` or `.call` in node. # https://stackoverflow.com/questions/3951840/how-to-invoke-a-function-on-an-object-dynamically-by-name # Octopi events to handle TIMER_START_EVENTS = [ "PrintStarted", "PrintResumed", ] # Update the interval loop TIMER_UPDATE_EVENTS = [ "SettingsUpdated" ] # End the loop events TIMER_STOP_EVENTS = [ "PrintFailed", "PrintDone", "PrintCancelling", "PrintCancelled", "PrintPaused", ] def execute_timer_event(self, event): if event in self.TIMER_START_EVENTS: self.start_printing_timer() elif event in self.TIMER_STOP_EVENTS: self.stop_printing_timer() elif event in self.TIMER_UPDATE_EVENTS: self.restart_printing_timer() else: return def on_event(self, event, payload): self.execute_timer_event(event) # Timer for interval printing_timer = None # Event for timer def printing_timer_tick(self): self._logger.debug("timer tick at interval %s", self._settings.get(["interval"])) snapshot_url = self.snapshot_to_s3() self.send_ifttt(snapshot_url) # Passed to timer as interval function to dynamically change interval. def printing_timer_interval(self): return int(self._settings.get(["interval"])) # Start def start_printing_timer(self, run_first = False): self._logger.debug("start timer") # Create and start the timer. self.printing_timer = RepeatedTimer( self.printing_timer_interval, self.printing_timer_tick, run_first = run_first ) self.printing_timer.start() # Stop def stop_printing_timer(self): self._logger.debug("stop timer") if self.printing_timer != None: self.printing_timer.cancel() self.printing_timer = None return # Restart def restart_printing_timer(self): self._logger.debug("restart timer") if self.printing_timer == None: return self.stop_printing_timer() self.start_printing_timer(True) def snapshot_to_s3(self): s3_bucket = self._settings.get(["s3_bucket_name"]) # Get the content-type for extension and a timestamp for key snapshot_url = self._settings.global_get(["webcam","snapshot"]) extension = guess_extension(head(snapshot_url).headers['content-type']) object_key = datetime.utcnow().strftime("%m-%d-%Y_%H:%M:%S") + extension # Create object s3_object = resource( 's3', aws_access_key_id=self._settings.get(["iam_access_key_id"]), aws_secret_access_key=self._settings.get(["iam_secret_access_key"]) ).Object(s3_bucket, object_key) # Stream to s3 with get(snapshot_url, stream=True) as r: s3_object.put( Body=r.content, ACL='public-read' ) return "https://%s.s3.amazonaws.com/%s" % (s3_bucket, object_key) def send_ifttt(self, snapshot_url): json = {'value1':'this is value_1','value2':'this is value_2','value3':snapshot_url} post(self._settings.get(["webhook_url"]), data=json) def get_template_configs(self): return [ dict(type="settings", custom_bindings=False) ] ##~~ AssetPlugin mixin def get_assets(self): # Define your plugin's asset files to automatically include in the # core UI here. return dict( js=["js/snap.js"], css=["css/snap.css"], less=["less/snap.less"] ) ##~~ 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( snap=dict( displayName="Snap Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Snap", current=self._plugin_version, # update method: pip pip="https://github.com/codyolsen/OctoPrint-Snap/archive/{target_version}.zip" ) )
class shutdownprinterPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin): def __init__(self): self.url = "" self._mode_shutdown_gcode = True self._mode_shutdown_api = False self._mode_shutdown_api_custom = False self.api_custom_GET = False self.api_custom_POST = False self.api_custom_url = "" self.api_custom_json_header = "" self.api_custom_body = "" self.api_key_plugin = "" self.api_json_command = "" self.api_plugin_name = "" self.api_plugin_port = 5000 self.abortTimeout = 0 self.temperatureValue = 0 self.temperatureTarget = False self.printFailed = False self.printCancelled = False self.rememberCheckBox = False self.lastCheckBoxValue = False self._shutdown_printer_enabled = True self._timeout_value = None self._abort_timer = None self._abort_timer_temp = None def initialize(self): self.url = self._settings.get(["url"]) self._logger.debug("url: %s" % self.url) self.api_key_plugin = self._settings.get(["api_key_plugin"]) self._logger.debug("api_key_plugin: %s" % self.api_key_plugin) self._mode_shutdown_gcode = self._settings.get_boolean( ["_mode_shutdown_gcode"]) self._logger.debug("_mode_shutdown_gcode: %s" % self._mode_shutdown_gcode) self._mode_shutdown_api = self._settings.get_boolean( ["_mode_shutdown_api"]) self._logger.debug("_mode_shutdown_api: %s" % self._mode_shutdown_api) self._mode_shutdown_api_custom = self._settings.get_boolean( ["_mode_shutdown_api_custom"]) self._logger.debug("_mode_shutdown_api_custom: %s" % self._mode_shutdown_api_custom) self.api_custom_POST = self._settings.get_boolean(["api_custom_POST"]) self._logger.debug("api_custom_POST: %s" % self.api_custom_POST) self.api_custom_GET = self._settings.get_boolean(["api_custom_GET"]) self._logger.debug("api_custom_GET: %s" % self.api_custom_GET) self.api_custom_url = self._settings.get(["api_custom_url"]) self._logger.debug("api_custom_url: %s" % self.api_custom_url) self.api_custom_json_header = self._settings.get( ["api_custom_json_header"]) self._logger.debug("api_custom_json_header: %s" % self.api_custom_json_header) self.api_custom_body = self._settings.get(["api_custom_body"]) self._logger.debug("api_custom_body: %s" % self.api_custom_body) self.api_json_command = self._settings.get(["api_json_command"]) self._logger.debug("api_json_command: %s" % self.api_json_command) self.api_plugin_name = self._settings.get(["api_plugin_name"]) self._logger.debug("api_plugin_name: %s" % self.api_plugin_name) self.api_plugin_port = self._settings.get_int(["api_plugin_port"]) self._logger.debug("api_plugin_port: %s" % self.api_plugin_port) self.temperatureValue = self._settings.get_int(["temperatureValue"]) self._logger.debug("temperatureValue: %s" % self.temperatureValue) self.temperatureTarget = self._settings.get_boolean( ["temperatureTarget"]) self._logger.debug("temperatureTarget: %s" % self.temperatureTarget) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._logger.debug("abortTimeout: %s" % self.abortTimeout) self.printFailed = self._settings.get_boolean(["printFailed"]) self._logger.debug("printFailed: %s" % self.printFailed) self.printCancelled = self._settings.get_boolean(["printCancelled"]) self._logger.debug("printCancelled: %s" % self.printCancelled) self.rememberCheckBox = self._settings.get_boolean( ["rememberCheckBox"]) self._logger.debug("rememberCheckBox: %s" % self.rememberCheckBox) self.lastCheckBoxValue = self._settings.get_boolean( ["lastCheckBoxValue"]) self._logger.debug("lastCheckBoxValue: %s" % self.lastCheckBoxValue) if self.rememberCheckBox: self._shutdown_printer_enabled = self.lastCheckBoxValue def get_assets(self): return dict(js=["js/shutdownprinter.js"], css=["css/shutdownprinter.css"]) def get_template_configs(self): return [ dict(type="sidebar", name="Shutdown Printer", custom_bindings=False, icon="power-off"), dict(type="settings", custom_bindings=False) ] def get_api_commands(self): return dict(enable=[], disable=[], shutdown=["mode"], abort=[]) def on_api_command(self, command, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if command == "enable": self._shutdown_printer_enabled = True elif command == "disable": self._shutdown_printer_enabled = False elif command == "shutdown": self._shutdown_printer_API_CMD( data["mode"] ) #mode 1 = gcode, mode 2 = api, mode 3 = custom api elif command == "abort": if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timeout_value = None self._logger.info("Shutdown aborted.") if command == "enable" or command == "disable": self.lastCheckBoxValue = self._shutdown_printer_enabled if self.rememberCheckBox: self._settings.set_boolean(["lastCheckBoxValue"], self.lastCheckBoxValue) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type="timeout", timeout_value=self._timeout_value)) def on_event(self, event, payload): if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type="timeout", timeout_value=self._timeout_value)) return if not self._shutdown_printer_enabled: return if event not in [ Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED ]: return if event == Events.PRINT_DONE: self._temperature_target() return elif event == Events.PRINT_CANCELLED and self.printCancelled: self._temperature_target() return elif event == Events.PRINT_FAILED and self.printFailed: self._temperature_target() return else: return def _temperature_target(self): if self._abort_timer_temp is not None: return if self.temperatureTarget: self._abort_timer_temp = RepeatedTimer(2, self._temperature_task) self._abort_timer_temp.start() else: self._timer_start() def _temperature_task(self): if self._printer.is_printing(): self._abort_timer_temp.cancel() self._abort_timer_temp = None return self._temp = self._printer.get_current_temperatures() tester = 0 number = 0 for tool in self._temp.keys(): if not tool == "bed": if self._temp[tool]["actual"] <= self.temperatureValue: tester += 1 number += 1 if tester == number: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timer_start() def _timer_start(self): if self._abort_timer is not None: return self._logger.info("Starting abort shutdown printer 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(shutdownprinterEnabled=self._shutdown_printer_enabled, type="timeout", timeout_value=self._timeout_value)) if self._printer.is_printing(): self._timeout_value = 0 self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type="timeout", timeout_value=self._timeout_value)) self._abort_timer.cancel() self._abort_timer = None return if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_printer() def _shutdown_printer(self): if self._mode_shutdown_gcode == True: self._shutdown_printer_by_gcode() elif self._mode_shutdown_api == True: self._shutdown_printer_by_API() else: self._shutdown_printer_by_API_custom() def _shutdown_printer_API_CMD(self, mode): if mode == 1: self._shutdown_printer_by_gcode() elif mode == 2: self._shutdown_printer_by_API() elif mode == 3: self._shutdown_printer_by_API_custom() def _shutdown_printer_by_API(self): url = "http://127.0.0.1:" + str( self.api_plugin_port) + "/api/plugin/" + self.api_plugin_name headers = { 'Content-Type': 'application/json', 'X-Api-Key': self.api_key_plugin } data = self.api_json_command self._logger.info("Shutting down printer with API") try: response = requests.post(url, headers=headers, data=data, verify=False, timeout=0.001) except requests.exceptions.Timeout: return except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return def _shutdown_printer_by_API_custom(self): headers = {} if self.api_custom_json_header != "": headers = json.loads(self.api_custom_json_header) if self.api_custom_POST == True: data = self.api_custom_body self._logger.info("Shutting down printer with API custom (POST)") try: response = requests.post(self.api_custom_url, headers=headers, data=data, verify=False, timeout=0.001) except requests.exceptions.Timeout: return except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return elif self.api_custom_GET == True: self._logger.info("Shutting down printer with API custom (GET)") try: response = requests.post(self.api_custom_url, headers=headers, verify=False, timeout=0.001) except requests.exceptions.Timeout: return except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return def _shutdown_printer_by_gcode(self): self._printer.commands("M81 " + self.url) self._logger.info("Shutting down printer with command: M81 " + self.url) def get_settings_defaults(self): return dict(url="", api_key_plugin="", abortTimeout=30, _mode_shutdown_gcode=True, _mode_shutdown_api=False, _mode_shutdown_api_custom=False, api_custom_POST=False, api_custom_GET=False, api_custom_url="", api_custom_json_header="", api_custom_body="", api_plugin_port=5000, temperatureValue=110, _shutdown_printer_enabled=True, printFailed=False, printCancelled=False, rememberCheckBox=False, lastCheckBoxValue=False) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.url = self._settings.get(["url"]) self.api_key_plugin = self._settings.get(["api_key_plugin"]) self._mode_shutdown_gcode = self._settings.get_boolean( ["_mode_shutdown_gcode"]) self._mode_shutdown_api = self._settings.get_boolean( ["_mode_shutdown_api"]) self._mode_shutdown_api_custom = self._settings.get_boolean( ["_mode_shutdown_api_custom"]) self.api_custom_POST = self._settings.get_boolean(["api_custom_POST"]) self.api_custom_GET = self._settings.get_boolean(["api_custom_GET"]) self.api_custom_url = self._settings.get(["api_custom_url"]) self.api_custom_json_header = self._settings.get( ["api_custom_json_header"]) self.api_custom_body = self._settings.get(["api_custom_body"]) self.api_json_command = self._settings.get(["api_json_command"]) self.api_plugin_name = self._settings.get(["api_plugin_name"]) self.api_plugin_port = self._settings.get_int(["api_plugin_port"]) self.temperatureValue = self._settings.get_int(["temperatureValue"]) self.temperatureTarget = self._settings.get_int(["temperatureTarget"]) self.printFailed = self._settings.get_boolean(["printFailed"]) self.printCancelled = self._settings.get_boolean(["printCancelled"]) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.rememberCheckBox = self._settings.get_boolean( ["rememberCheckBox"]) self.lastCheckBoxValue = self._settings.get_boolean( ["lastCheckBoxValue"]) def get_update_information(self): return dict(shutdownprinter=dict( displayName="Shutdown Printer", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-ShutdownPrinter", current=self._plugin_version, # update method: pip w/ dependency links pip= "https://github.com/devildant/OctoPrint-ShutdownPrinter/archive/{target_version}.zip" ))
class AutomaticshutdownPlugin(octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin): def __init__(self): self.abortTimeout = 0 self.rememberCheckBox = False self.lastCheckBoxValue = False self._automatic_shutdown_enabled = False self._timeout_value = None self._abort_timer = None self._wait_for_timelapse_timer = None def initialize(self): self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._logger.debug("abortTimeout: %s" % self.abortTimeout) self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) self._logger.debug("rememberCheckBox: %s" % self.rememberCheckBox) self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) self._logger.debug("lastCheckBoxValue: %s" % self.lastCheckBoxValue) if self.rememberCheckBox: self._automatic_shutdown_enabled = self.lastCheckBoxValue def get_assets(self): return dict(js=["js/automaticshutdown.js"]) def get_template_configs(self): return [dict(type="sidebar", name="Automatic Shutdown", custom_bindings=False, icon="power-off"), dict(type="settings", custom_bindings=False)] def get_api_commands(self): return dict(enable=[], disable=[], abort=[]) def on_api_command(self, command, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if command == "enable": self._automatic_shutdown_enabled = True elif command == "disable": self._automatic_shutdown_enabled = False elif command == "abort": if self._wait_for_timelapse_timer is not None: self._wait_for_timelapse_timer.cancel() self._wait_for_timelapse_timer = None if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._logger.info("Shutdown aborted.") if command == "enable" or command == "disable": self.lastCheckBoxValue = self._automatic_shutdown_enabled if self.rememberCheckBox: self._settings.set_boolean(["lastCheckBoxValue"], self.lastCheckBoxValue) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) def on_event(self, event, payload): if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) return if not self._automatic_shutdown_enabled: return if not self._settings.global_get(["server", "commands", "systemShutdownCommand"]): self._logger.warning("systemShutdownCommand is not defined. Aborting shutdown...") return if event not in [Events.PRINT_DONE, Events.PRINT_FAILED]: return if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error(): #Cancelled job return if event in [Events.PRINT_DONE, Events.PRINT_FAILED]: webcam_config = self._settings.global_get(["webcam", "timelapse"], merged=True) timelapse_type = webcam_config["type"] if (timelapse_type is not None and timelapse_type != "off"): self._wait_for_timelapse_start() else: self._timer_start() return def _wait_for_timelapse_start(self): if self._wait_for_timelapse_timer is not None: return self._wait_for_timelapse_timer = RepeatedTimer(5, self._wait_for_timelapse) self._wait_for_timelapse_timer.start() def _wait_for_timelapse(self): c = len(octoprint.timelapse.get_unrendered_timelapses()) if c > 0: self._logger.info("Waiting for %s timelapse(s) to finish rendering before starting shutdown timer..." % c) else: self._timer_start() def _timer_start(self): if self._abort_timer is not None: return if self._wait_for_timelapse_timer is not None: self._wait_for_timelapse_timer.cancel() self._logger.info("Starting abort shutdown 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(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._wait_for_timelapse_timer is not None: self._wait_for_timelapse_timer.cancel() self._wait_for_timelapse_timer = None if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): shutdown_command = self._settings.global_get(["server", "commands", "printerShutdownCommand"]) self._logger.info("Shutting down system with command: {command}".format(command=shutdown_command)) try: import sarge p = sarge.run(shutdown_command, async=True) except Exception as e: self._logger.exception("Error when shutting down: {error}".format(error=e)) return def get_settings_defaults(self): return dict( abortTimeout = 30, rememberCheckBox = False, lastCheckBoxValue = False ) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) def get_update_information(self): return dict( automaticshutdown=dict( displayName="Automatic Shutdown", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-AutomaticShutdown", current=self._plugin_version, # update method: pip w/ dependency links pip="https://github.com/OctoPrint/OctoPrint-AutomaticShutdown/archive/{target_version}.zip" ) )
class NavBarPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SettingsPlugin): def __init__(self): # Array of raspberry pi SoC's to check against, saves having a large if/then statement later self.piSocTypes = (["BCM2708", "BCM2709", "BCM2835", "BCM2711"]) self.debugMode = False # to simulate temp on Win/Mac self.displayRaspiTemp = None self._checkTempTimer = None self._checkCmdTimer = None self.sbc = SBC() self.cmd = None self.cmd_name = None def on_after_startup(self): self.displayRaspiTemp = self._settings.get(["displayRaspiTemp"]) self.piSocTypes = self._settings.get(["piSocTypes"]) self.cmd = self._settings.get(["cmd"]) self.cmd_name = self._settings.get(["cmd_name"]) self._logger.debug("Custom cmd name %r" % self.cmd_name) self._logger.debug("Custom cmd %r" % self.cmd) if sys.platform.startswith("linux"): self.sbc = SBCFactory().factory(self._logger) if self.debugMode: self.sbc.is_supported = True self.sbc.debugMode = True if self.sbc.is_supported and self.displayRaspiTemp: self._logger.debug("Let's start RepeatedTimer!") interval = 5.0 if self.debugMode else 30.0 self.start_soc_timer(interval) if self.cmd_name: interval = 5.0 if self.debugMode else 30.0 self.start_custom_timer(interval) try: self._logger.debug("is supported? - %s" % self.sbc.is_supported) except Exception: self._logger.debug("Embeded platform is not detected") def start_soc_timer(self, interval): self._checkTempTimer = RepeatedTimer(interval, self.update_soc_temp, run_first=True) self._checkTempTimer.start() def start_custom_timer(self, interval): self._checkCmdTimer = RepeatedTimer(interval, self.update_custom, run_first=True) self._checkCmdTimer.start() def update_soc_temp(self): temp = self.sbc.check_soc_temp() self._logger.debug("match: %s" % temp) self._plugin_manager.send_plugin_message(self._identifier, dict(isSupported=self.sbc.is_supported, soctemp=temp)) def update_custom(self): cmd_rtv = self.get_custom_result() self._plugin_manager.send_plugin_message(self._identifier, dict(cmd_result=cmd_rtv, cmd_name=self.cmd_name)) def get_custom_result(self): if self.cmd: try: cmd_rtv = str(os.popen(self.cmd).read()) self._logger.debug("cmd_rtv: %s" % cmd_rtv) return cmd_rtv except Exception: self._logger.debug("cmd error") return "" # ~~ SettingsPlugin def get_settings_defaults(self): return dict(displayRaspiTemp=True, piSocTypes=self.piSocTypes, cmd="", cmd_name="", useShortNames=False, makeMoreRoom=False, soc_name="SoC", ) def on_settings_save(self, data): diff = super(NavBarPlugin, self).on_settings_save(data) self._logger.debug("data: " + str(data)) if "displayRaspiTemp" in data: self.displayRaspiTemp = data["displayRaspiTemp"] if self.displayRaspiTemp: interval = 5.0 if self.debugMode else 30.0 self.start_soc_timer(interval) else: if self._checkTempTimer is not None: try: self._checkTempTimer.cancel() except Exceptionx: pass if "cmd" in data: self.cmd = data["cmd"] self.cmd_name = data["cmd_name"] if self.cmd: interval = 5.0 if self.debugMode else 30.0 self.start_custom_timer(interval) else: if self._checkCmdTimer is not None: try: self._checkCmdTimer.cancel() except Exceptionx: pass self._plugin_manager.send_plugin_message(self._identifier, dict()) return diff # ~~ TemplatePlugin API def get_template_configs(self): try: # Todo: settings have to be fixed # if self.sbc.is_supported: # return [ # dict(type="settings", template="navbartemp_settings_sbc.jinja2") # ] # else: return [dict(type="settings", template="navbartemp_settings.jinja2")] except Exception: return [] # ~~ AssetPlugin API def get_assets(self): return { "js": ["js/navbartemp.js"], "css": ["css/navbartemp.css"], "less": ["less/navbartemp.less"] } # ~~ Softwareupdate hook def get_update_information(self): return dict( navbartemp=dict( displayName="Navbar Temperature Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-NavbarTemp", current=self._plugin_version, # update method: pip w/ dependency links pip="https://github.com/imrahil/OctoPrint-NavbarTemp/archive/{target_version}.zip" ) )
class shutdownprinterPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin): def __init__(self): self.url = "" self._typeNotifShow = "" self._wait_temp = "" self.previousEventIsCancel = False self._abort_all_for_this_session = False self.gcode = "M81" self._mode_shutdown_gcode = True self._mode_shutdown_api = False self._mode_shutdown_api_custom = False self.api_custom_GET = False self.api_custom_POST = False self.api_custom_PUT = False self.api_custom_url = "" self.api_custom_json_header = "" self.api_custom_body = "" self.api_key_plugin = "" self.api_json_command = "" self.api_plugin_name = "" self.api_plugin_port = 5000 self.extraCommand = "" self.abortTimeout = 0 self.temperatureValue = 0 self.temperatureTarget = False self.printFailed = False self.printCancelled = False self.rememberCheckBox = False self.lastCheckBoxValue = False self._shutdown_printer_enabled = True self._timeout_value = None self._abort_timer = None self._abort_timer_temp = None self.ctx = ssl.create_default_context() self.ctx.check_hostname = False self.ctx.verify_mode = ssl.CERT_NONE def initialize(self): self.gcode = self._settings.get(["gcode"]) self._logger.debug("gcode: %s" % self.gcode) self.url = self._settings.get(["url"]) self._logger.debug("url: %s" % self.url) self.api_key_plugin = self._settings.get(["api_key_plugin"]) self._logger.debug("api_key_plugin: %s" % self.api_key_plugin) self._mode_shutdown_gcode = self._settings.get_boolean( ["_mode_shutdown_gcode"]) self._logger.debug("_mode_shutdown_gcode: %s" % self._mode_shutdown_gcode) self._mode_shutdown_api = self._settings.get_boolean( ["_mode_shutdown_api"]) self._logger.debug("_mode_shutdown_api: %s" % self._mode_shutdown_api) self._mode_shutdown_api_custom = self._settings.get_boolean( ["_mode_shutdown_api_custom"]) self._logger.debug("_mode_shutdown_api_custom: %s" % self._mode_shutdown_api_custom) self.api_custom_POST = self._settings.get_boolean(["api_custom_POST"]) self._logger.debug("api_custom_POST: %s" % self.api_custom_POST) self.api_custom_GET = self._settings.get_boolean(["api_custom_GET"]) self._logger.debug("api_custom_GET: %s" % self.api_custom_GET) self.api_custom_PUT = self._settings.get_boolean(["api_custom_PUT"]) self._logger.debug("api_custom_PUT: %s" % self.api_custom_PUT) self.api_custom_url = self._settings.get(["api_custom_url"]) self._logger.debug("api_custom_url: %s" % self.api_custom_url) self.api_custom_json_header = self._settings.get( ["api_custom_json_header"]) self._logger.debug("api_custom_json_header: %s" % self.api_custom_json_header) self.api_custom_body = self._settings.get(["api_custom_body"]) self._logger.debug("api_custom_body: %s" % self.api_custom_body) self.api_json_command = self._settings.get(["api_json_command"]) self._logger.debug("api_json_command: %s" % self.api_json_command) self.api_plugin_name = self._settings.get(["api_plugin_name"]) self._logger.debug("api_plugin_name: %s" % self.api_plugin_name) self.api_plugin_port = self._settings.get_int(["api_plugin_port"]) self._logger.debug("api_plugin_port: %s" % self.api_plugin_port) self.temperatureValue = self._settings.get_int(["temperatureValue"]) self._logger.debug("temperatureValue: %s" % self.temperatureValue) self.temperatureTarget = self._settings.get_boolean( ["temperatureTarget"]) self._logger.debug("temperatureTarget: %s" % self.temperatureTarget) self.extraCommand = self._settings.get(["extraCommand"]) self._logger.debug("extraCommand: %s" % self.extraCommand) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._logger.debug("abortTimeout: %s" % self.abortTimeout) self.printFailed = self._settings.get_boolean(["printFailed"]) self._logger.debug("printFailed: %s" % self.printFailed) self.printCancelled = self._settings.get_boolean(["printCancelled"]) self._logger.debug("printCancelled: %s" % self.printCancelled) self.rememberCheckBox = self._settings.get_boolean( ["rememberCheckBox"]) self._logger.debug("rememberCheckBox: %s" % self.rememberCheckBox) self.lastCheckBoxValue = self._settings.get_boolean( ["lastCheckBoxValue"]) self._logger.debug("lastCheckBoxValue: %s" % self.lastCheckBoxValue) if self.rememberCheckBox: self._shutdown_printer_enabled = self.lastCheckBoxValue self.shutdown_printer = self._plugin_manager.get_hooks( "octoprint.plugin.ShutdownPrinter.shutdown") self.enclosure_screen_hook = self._plugin_manager.get_hooks( "octoprint.plugin.external.event") self.hookEnclosureScreenfct() def on_after_startup(self): self.hookEnclosureScreenfct() def hookEnclosureScreenfct(self, data=dict()): self._logger.error("send status off 1") if self.enclosure_screen_hook is not None: for name, hook in self.enclosure_screen_hook.items(): # first sd card upload plugin that feels responsible gets the job try: # hook(self.statusManualStop) self._logger.error("send status off 2") hook( dict(shutdownPrinter=dict( offAfterPrintEnd=self._shutdown_printer_enabled, data=data))) except Exception as e: self._logger.error("Failed get hook: %s" % e.message) else: self._logger.error("hook does not exist") def hook_event_enclosureScreen(self, data): if "shutdownPrinter" in data: self._logger.info(str(data["shutdownPrinter"])) if "offAfterPrintEnd" in data["shutdownPrinter"]: if self._shutdown_printer_enabled: self._shutdown_printer_enabled = False else: self._shutdown_printer_enabled = True self.lastCheckBoxValue = self._shutdown_printer_enabled if self.rememberCheckBox: self._settings.set_boolean(["lastCheckBoxValue"], self.lastCheckBoxValue) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type=self._typeNotifShow, timeout_value=self._timeout_value, wait_temp=self._wait_temp, time=time.time())) if "abort" in data["shutdownPrinter"]: self.forcedAbort = True if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timeout_value = None self._typeNotifShow = "destroynotif" self._timeout_value = -1 self._wait_temp = "" self._logger.info("Shutdown aborted.") self._destroyNotif() def get_assets(self): return dict(js=["js/shutdownprinter.js"], css=["css/shutdownprinter.css"]) def get_template_configs(self): return [ dict(type="sidebar", name="Shutdown Printer", custom_bindings=False, icon="power-off"), dict(type="settings", custom_bindings=False) ] def get_api_commands(self): return dict(enable=["eventView"], status=[], update=["eventView"], disable=["eventView"], shutdown=["mode", "eventView"], abort=["eventView"]) def get_additional_permissions(self): return [ dict(key="ADMIN", name="shutdown printer plugin", description=gettext( "Allows to access to api of plugin shutdownprinter."), default_groups=[ADMIN_GROUP], roles=["admin"], dangerous=True) ] def on_api_command(self, command, data): # if not user_permission.can(): # return make_response("Insufficient rights", 403) try: plugin_permission = Permissions.PLUGIN_SHUTDOWNPRINTER_ADMIN.can() except: plugin_permission = user_permission.can() if not plugin_permission: return make_response("Insufficient rights", 403) # user_id = current_user.get_name() # if not user_id or not admin_permission.can(): # return make_response("Insufficient rights", 403) if command == "status": return make_response(str(self._shutdown_printer_enabled), 200) elif command == "enable": self._shutdown_printer_enabled = True self.hookEnclosureScreenfct() elif command == "disable": self._shutdown_printer_enabled = False self.hookEnclosureScreenfct() elif command == "shutdown": def process(): try: self._shutdown_printer_API_CMD( data["mode"] ) #mode 1 = gcode, mode 2 = api, mode 3 = custom api except: # exc_type, exc_obj, exc_tb = sys.exc_info() self._logger.error("Failed read tty screen : %s" % str(traceback.format_exc())) thread = threading.Thread(target=process) thread.daemon = True self._logger.info("start thread") thread.start() elif command == "abort": self.forcedAbort = True if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timeout_value = None self._typeNotifShow = "destroynotif" self._timeout_value = -1 self._wait_temp = "" self._logger.info("Shutdown aborted.") self._destroyNotif() if command == "enable" or command == "disable": self.lastCheckBoxValue = self._shutdown_printer_enabled if self.rememberCheckBox: self._settings.set_boolean(["lastCheckBoxValue"], self.lastCheckBoxValue) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) self._logger.info("eventView") if data["eventView"] == True: self.sendNotif(True) return make_response("ok", 200) def sendNotif(self, skipHook=False): if self.forcedAbort == False: if skipHook == False: self.hookEnclosureScreenfct( dict(type=self._typeNotifShow, timeout_value=self._timeout_value, wait_temp=self._wait_temp, time=time.time())) self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type=self._typeNotifShow, timeout_value=self._timeout_value, wait_temp=self._wait_temp, time=time.time())) def on_event(self, event, payload): # if event == Events.CLIENT_OPENED: # self._plugin_manager.send_plugin_message(self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type="timeout", timeout_value=self._timeout_value)) # return if not self._shutdown_printer_enabled: return if event == Events.PRINT_STARTED: # self._logger.info("Print started") self.previousEventIsCancel = False self._abort_all_for_this_session = False if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._destroyNotif() self._logger.info( "ShutdownPrinter Events.PRINT_STARTED aborted because PRINTING detect" ) if event not in [ Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED ]: return return if event == Events.PRINT_DONE: # self._logger.info("Print done") self._temperature_target() return elif event == Events.PRINT_CANCELLED and self.printCancelled: # self._logger.info("Print cancelled") self.previousEventIsCancel = True self._temperature_target() return elif event == Events.PRINT_CANCELLED: # self._logger.info("Print cancelled") self.previousEventIsCancel = True return elif (event == Events.ERROR or event == Events.PRINT_FAILED) and self.printFailed: if self.previousEventIsCancel == True: self.previousEventIsCancel = False return # self._logger.info("Print failed") self._temperature_target() return else: self.previousEventIsCancel = False return def _temperature_target(self): self.forcedAbort = False if self._abort_timer_temp is not None: # self._logger.info("_abort_timer_temp_destroyNotif") self._destroyNotif() return if self._abort_all_for_this_session == True: # self._logger.info("_abort_all_for_this_session_destroyNotif") if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._destroyNotif() return if self.temperatureTarget: self._abort_timer_temp = RepeatedTimer(2, self._temperature_task) self._abort_timer_temp.start() else: self._timer_start() def _temperature_task(self): try: if self._abort_all_for_this_session == True: if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._destroyNotif() return if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._logger.info( "ShutdownPrinter _temperature_task aborted because PRINTING detect" ) self._destroyNotif() return self._temp = self._printer.get_current_temperatures() self._logger.info(str(self._temp)) tester = 0 number = 0 self._wait_temp = "" self._typeNotifShow = "waittemp" for tool in self._temp.keys(): if not tool == "bed" and not tool == "chamber": if self._temp[tool]["actual"] is None: continue if self._temp[tool]["actual"] <= self.temperatureValue: tester += 1 number += 1 self._wait_temp = " - " + str(tool) + ": " + str( self._temp[tool]["actual"]) + "/" + str( self.temperatureValue) + "°C\n" if tester == number: if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timer_start() else: self.sendNotif() except: self._logger.error("Failed to connect to call api: %s" % str(traceback.format_exc())) def _timer_start(self): if self._abort_timer is not None: self._destroyNotif() return if self._abort_all_for_this_session == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._destroyNotif() return self._logger.info("Starting abort shutdown printer 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: self._destroyNotif() return if self._abort_all_for_this_session == True: self._destroyNotif() if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None return if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._logger.info( "ShutdownPrinter _timer_task aborted because PRINTING detect") self._destroyNotif() return self._timeout_value -= 1 self._typeNotifShow = "timeout" self._wait_temp = "" self.sendNotif() if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self.forcedAbort == False: self._shutdown_printer() def _destroyNotif(self): self._timeout_value = -1 self._wait_temp = "" self.hookEnclosureScreenfct( dict(type="destroynotif", timeout_value=-1, wait_temp="", time=time.time())) self._plugin_manager.send_plugin_message( self._identifier, dict(shutdownprinterEnabled=self._shutdown_printer_enabled, type="destroynotif", timeout_value=-1, wait_temp="", time=time.time())) def _shutdown_printer(self): if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: time.sleep(2) if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: self._logger.info( "ShutdownPrinter aborted because PRINTING detect") return if self.forcedAbort: self._logger.info( "ShutdownPrinter aborted because force abort is True") return if self._mode_shutdown_gcode == True: self._shutdown_printer_by_gcode() elif self._mode_shutdown_api == True: self._shutdown_printer_by_API() else: self._shutdown_printer_by_API_custom() def _shutdown_printer_API_CMD(self, mode): if mode == 1: self._shutdown_printer_by_gcode() elif mode == 2: self._shutdown_printer_by_API() elif mode == 3: self._shutdown_printer_by_API_custom() def _extraCommand(self): if self.shutdown_printer is not None: for name, hook in self.shutdown_printer.items(): # first sd card upload plugin that feels responsible gets the job try: hook() except Exception as e: self._logger.error("Failed get hook: %s" % e.message) else: self._logger.error("hook does not exist") if self.extraCommand != "": process = subprocess.Popen(self.extraCommand, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() self._logger.info("extraCommand stdout: %s" % stdout.rstrip().strip()) self._logger.info("extraCommand stderr: %s" % stderr.rstrip().strip()) def _shutdown_printer_by_API(self): if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: time.sleep(2) if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: self._logger.info( "ShutdownPrinter by API aborted because PRINTING detect") return if self.forcedAbort: self._logger.info( "ShutdownPrinter by API aborted because force abort is True") return url = "http://127.0.0.1:" + str( self.api_plugin_port) + "/api/plugin/" + self.api_plugin_name headers = { 'Content-Type': 'application/json', 'X-Api-Key': self.api_key_plugin } data = self.api_json_command.encode() self._logger.info("Shutting down printer with API") try: request = urllib2.Request(url, data=data, headers=headers) request.get_method = lambda: "POST" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE contents = urllib2.urlopen(request, timeout=30, context=ctx).read() self._logger.debug("call response (POST API octoprint): %s" % contents) self._extraCommand() except: self._logger.error("Failed to connect to call api: %s" % str(traceback.format_exc())) return def _shutdown_printer_by_API_custom(self): if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: time.sleep(2) if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: self._logger.info( "ShutdownPrinter by Custom API aborted because PRINTING detect" ) return if self.forcedAbort: self._logger.info( "ShutdownPrinter by Custom API aborted because force abort is True" ) return headers = {} if self.api_custom_json_header != "": headers = eval(self.api_custom_json_header) if self.api_custom_PUT == True: data = self.api_custom_body.encode() self._logger.info("Shutting down printer with API custom (PUT)") try: request = urllib2.Request(self.api_custom_url, data=data, headers=headers) request.get_method = lambda: "PUT" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE contents = urllib2.urlopen(request, timeout=30, context=ctx).read() self._logger.debug("call response (PUT): %s" % contents) self._extraCommand() except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return if self.api_custom_POST == True: data = self.api_custom_body self._logger.info("Shutting down printer with API custom (POST)") try: request = urllib2.Request(self.api_custom_url, data=data, headers=headers) request.get_method = lambda: "POST" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE contents = urllib2.urlopen(request, timeout=30, context=ctx).read() self._logger.debug("call response (POST): %s" % contents) self._extraCommand() except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return elif self.api_custom_GET == True: self._logger.info("Shutting down printer with API custom (GET)") try: request = urllib2.Request(self.api_custom_url, headers=headers) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE contents = urllib2.urlopen(request, timeout=30, context=ctx).read() self._logger.debug("call response (GET): %s" % contents) self._extraCommand() except Exception as e: self._logger.error("Failed to connect to call api: %s" % e.message) return def _shutdown_printer_by_gcode(self): if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: time.sleep(2) if self._printer.get_state_id( ) == "PRINTING" and self._printer.is_printing() == True: self._logger.info( "ShutdownPrinter by GCODE aborted because PRINTING detect") return if self.forcedAbort: self._logger.info( "ShutdownPrinter by GCODE aborted because force abort is True") return self._printer.commands(self.gcode + " " + self.url) self._logger.info("Shutting down printer with command: " + self.gcode + " " + self.url) self._extraCommand() def powersupplyCancelAutoShutdown(self, status): self._logger.info("Shutdown aborted status " + str(status)) if status == 0: self.forcedAbort = True self._abort_all_for_this_session = True if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None if self._abort_timer_temp is not None: self._abort_timer_temp.cancel() self._abort_timer_temp = None self._timeout_value = None self._logger.info("Shutdown aborted.") self._destroyNotif() def get_settings_defaults(self): return dict(gcode="M81", url="", api_key_plugin="", abortTimeout=30, extraCommand="", _mode_shutdown_gcode=True, _mode_shutdown_api=False, _mode_shutdown_api_custom=False, api_custom_POST=False, api_custom_GET=False, api_custom_PUT=False, api_custom_url="", api_custom_json_header="", api_custom_body="", api_plugin_port=5000, temperatureValue=110, _shutdown_printer_enabled=True, printFailed=False, printCancelled=False, rememberCheckBox=False, lastCheckBoxValue=False) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.gcode = self._settings.get(["gcode"]) self.url = self._settings.get(["url"]) self.api_key_plugin = self._settings.get(["api_key_plugin"]) self._mode_shutdown_gcode = self._settings.get_boolean( ["_mode_shutdown_gcode"]) self._mode_shutdown_api = self._settings.get_boolean( ["_mode_shutdown_api"]) self._mode_shutdown_api_custom = self._settings.get_boolean( ["_mode_shutdown_api_custom"]) self.api_custom_POST = self._settings.get_boolean(["api_custom_POST"]) self.api_custom_GET = self._settings.get_boolean(["api_custom_GET"]) self.api_custom_PUT = self._settings.get_boolean(["api_custom_PUT"]) self.api_custom_url = self._settings.get(["api_custom_url"]) self.api_custom_json_header = self._settings.get( ["api_custom_json_header"]) self.api_custom_body = self._settings.get(["api_custom_body"]) self.api_json_command = self._settings.get(["api_json_command"]) self.api_plugin_name = self._settings.get(["api_plugin_name"]) self.api_plugin_port = self._settings.get_int(["api_plugin_port"]) self.temperatureValue = self._settings.get_int(["temperatureValue"]) self.temperatureTarget = self._settings.get_int(["temperatureTarget"]) self.extraCommand = self._settings.get(["extraCommand"]) self.printFailed = self._settings.get_boolean(["printFailed"]) self.printCancelled = self._settings.get_boolean(["printCancelled"]) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.rememberCheckBox = self._settings.get_boolean( ["rememberCheckBox"]) self.lastCheckBoxValue = self._settings.get_boolean( ["lastCheckBoxValue"]) def get_update_information(self): return dict(shutdownprinter=dict( displayName="Shutdown Printer", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-ShutdownPrinter", current=self._plugin_version, # update method: pip w/ dependency links pip= "https://github.com/devildant/OctoPrint-ShutdownPrinter/archive/{target_version}.zip" ))
class Display_panelPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.ShutdownPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.ProgressPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin): _area_offset = 3 _cancel_requested_at = 0 _cancel_timer = None _colored_strip_height = 16 # height of colored strip on top for dual color display _debounce = 0 _display_init = False _displaylayerprogress_current_height = -1.0 _displaylayerprogress_current_layer = -1 _displaylayerprogress_total_height = -1.0 _displaylayerprogress_total_layer = -1 _display_timeout_active = False _display_timeout_option = 0 # -1 - deactivated, 0 - printer disconnected, 1 - disconnected/connected but idle, 2 - always _display_timeout_time = 0 _display_timeout_timer = None _etl_format = "{hours:02d}h {minutes:02d}m {seconds:02d}s" _eta_strftime = "" _gpio_init = False _image_rotate = False _last_debounce = 0 _last_display_timeout_option = 0 # -1 - deactivated, 0 - printer disconnected, 1 - disconnected/connected but idle, 2 - always _last_display_timeout_time = 0 _last_i2c_address = "" _last_image_rotate = False _last_pin_cancel = -1 _last_pin_mode = -1 _last_pin_pause = -1 _last_pin_play = -1 _last_printer_state = 0 # 0 - disconnected, 1 - connected but idle, 2 - printing _pin_cancel = -1 _pin_mode = -1 _pin_pause = -1 _pin_play = -1 _printer_state = 0 # 0 - disconnected, 1 - connected but idle, 2 - printing _progress_on_top = False _screen_mode = ScreenModes.SYSTEM _system_stats = {} _timebased_progress = False ##~~ StartupPlugin mixin def on_after_startup(self): """ StartupPlugin lifecycle hook, called after Octoprint startup is complete """ self.setup_display() self.setup_gpio() self.configure_gpio() self.clear_display() self.check_system_stats() self.start_system_timer() self.update_ui() self.start_display_timer() ##~~ ShutdownPlugin mixin def on_shutdown(self): """ ShutdownPlugin lifecycle hook, called before Octoprint shuts down """ self.stop_display_timer() self.clear_display() self.clean_gpio() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): """ EventHandlerPlugin lifecycle hook, called whenever an event is fired """ # self._logger.info("on_event: %s", event) self.set_printer_state(event) # Connectivity if event == Events.DISCONNECTED: self._screen_mode = ScreenModes.SYSTEM if event in (Events.CONNECTED, Events.CONNECTING, Events.CONNECTIVITY_CHANGED, Events.DISCONNECTING): self.update_ui() # Print start display if event == Events.PRINT_STARTED: self._screen_mode = ScreenModes.PRINT self.update_ui() # Print end states if event in (Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_CANCELLING): self._displaylayerprogress_current_height = -1.0 self._displaylayerprogress_current_layer = -1 self._displaylayerprogress_total_height = -1.0 self._displaylayerprogress_total_layer = -1 self.update_ui() # Print limbo states if event in (Events.PRINT_PAUSED, Events.PRINT_RESUMED): self.update_ui() # Mid-print states if event in (Events.Z_CHANGE, Events.PRINTER_STATE_CHANGED): self.update_ui() # Get progress information from DisplayLayerProgress plugin if event in ("DisplayLayerProgress_heightChanged", "DisplayLayerProgress_layerChanged"): if payload.get('currentHeight') != "-": self._displaylayerprogress_current_height = float(payload.get('currentHeight')) else: self._displaylayerprogress_current_height = -1.0 if payload.get('currentLayer') != "-": self._displaylayerprogress_current_layer = int(payload.get('currentLayer')) else: self._displaylayerprogress_current_layer = -1 self._displaylayerprogress_total_height = float(payload.get('totalHeight')) self._displaylayerprogress_total_layer = int(payload.get('totalLayer')) self.update_ui() ##~~ ProgressPlugin mixin def on_print_progress(self, storage, path, progress): """ ProgressPlugin lifecycle hook, called when print progress changes, at most in 1% incremements """ self.update_ui() def on_slicing_progress(self, slicer, source_location, source_path, destination_location, destination_path, progress): """ ProgressPlugin lifecycle hook, called whenever slicing progress changes, at most in 1% increments """ self._logger.info("on_slicing_progress: %s", progress) # TODO: Handle slicing progress bar self.update_ui() ##~~ TemplatePlugin mixin def get_template_configs(self): """ TemplatePlugin lifecycle hook, called to get templated settings """ return [dict(type="settings", custom_bindings=False)] ##~~ SettingsPlugin mixin def initialize(self): """ Prepare variables for setting modification detection """ self._debounce = int(self._settings.get(["debounce"])) self._display_init = False self._display_timeout_option = int(self._settings.get(["display_timeout_option"])) self._display_timeout_time = int(self._settings.get(["display_timeout_time"])) self._eta_strftime = str(self._settings.get(["eta_strftime"])) self._gpio_init = False self._i2c_address = str(self._settings.get(["i2c_address"])) self._image_rotate = bool(self._settings.get(["image_rotate"])) self._pin_cancel = int(self._settings.get(["pin_cancel"])) self._pin_mode = int(self._settings.get(["pin_mode"])) self._pin_pause = int(self._settings.get(["pin_pause"])) self._pin_play = int(self._settings.get(["pin_play"])) self._progress_on_top = bool(self._settings.get(["progress_on_top"])) self._screen_mode = ScreenModes.SYSTEM self._last_debounce = self._debounce self._last_display_timeout_option = self._display_timeout_option self._last_display_timeout_time = self._display_timeout_time self._last_i2c_address = self._i2c_address self._last_image_rotate = False self._last_pin_cancel = self._pin_cancel self._last_pin_mode = self._pin_mode self._last_pin_pause = self._pin_pause self._last_pin_play = self._pin_play self._timebased_progress = bool(self._settings.get(["timebased_progress"])) def get_settings_defaults(self): """ SettingsPlugin lifecycle hook, called to get default settings """ return dict( debounce = 250, # Debounce 250ms display_timeout_option = -1, # Default is never display_timeout_time = 5, # Default is 5 minutes eta_strftime = "%-m/%d %-I:%M%p", # Default is month/day hour:minute + AM/PM i2c_address = "0x3c", # Default is hex address 0x3c image_rotate = False, # Default if False (no rotation) pin_cancel = -1, # Default is disabled pin_mode = -1, # Default is disabled pin_pause = -1, # Default is disabled pin_play = -1, # Default is disabled progress_on_top = False, # Default is disabled timebased_progress = False, # Default is disabled ) def on_settings_save(self, data): """ SettingsPlugin lifecycle hook, called when settings are saved """ octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._debounce = int(self._settings.get(["debounce"])) self._display_timeout_option = int(self._settings.get(["display_timeout_option"])) self._display_timeout_time = int(self._settings.get(["display_timeout_time"])) self._eta_strftime = str(self._settings.get(["eta_strftime"])) self._i2c_address = str(self._settings.get(["i2c_address"])) self._image_rotate = bool(self._settings.get(["image_rotate"])) self._pin_cancel = int(self._settings.get(["pin_cancel"])) self._pin_mode = int(self._settings.get(["pin_mode"])) self._pin_pause = int(self._settings.get(["pin_pause"])) self._pin_play = int(self._settings.get(["pin_play"])) pins_updated = 0 try: if self._i2c_address.lower() != self._last_i2c_address.lower() or \ self._image_rotate != self._last_image_rotate: self.clear_display() self._display_init = False self._last_i2c_address = self._i2c_address self._last_image_rotate = self._image_rotate self.setup_display() self.clear_display() self.check_system_stats() self.start_system_timer() self._screen_mode = ScreenModes.SYSTEM self.update_ui() if self._pin_cancel != self._last_pin_cancel: pins_updated = pins_updated + 1 self.clean_single_gpio(self._last_pin_cancel) if self._pin_mode != self._last_pin_mode: pins_updated = pins_updated + 2 self.clean_single_gpio(self._last_pin_mode) if self._pin_pause != self._last_pin_pause: pins_updated = pins_updated + 4 self.clean_single_gpio(self._last_pin_pause) if self._pin_play != self._last_pin_play: pins_updated = pins_updated + 8 self.clean_single_gpio(self._last_pin_play) self._gpio_init = False self.setup_gpio() if pins_updated == (pow(2, 4) - 1) or self._debounce != self._last_debounce: self.configure_gpio() pins_updated = 0 if pins_updated >= pow(2, 3): self.configure_single_gpio(self._pin_play) pins_updated = pins_updated - pow(2, 3) if pins_updated >= pow(2, 2): self.configure_single_gpio(self._pin_pause) pins_updated = pins_updated - pow(2, 2) if pins_updated >= pow(2, 1): self.configure_single_gpio(self._pin_mode) pins_updated = pins_updated - pow(2, 1) if pins_updated >= pow(2, 0): self.configure_single_gpio(self._pin_cancel) pins_updated = pins_updated - pow(2, 0) if pins_updated > 0: self.log_error("Something went wrong counting updated GPIO pins") if self._display_timeout_option != self._last_display_timeout_option or \ self._display_timeout_time != self._last_display_timeout_time: self.start_display_timer(self._display_timeout_time != self._last_display_timeout_time) self._last_debounce = self._debounce self._last_display_timeout_option = self._display_timeout_option self._last_display_timeout_time = self._display_timeout_time self._last_pin_cancel = self._pin_cancel self._last_pin_mode = self._pin_mode self._last_pin_play = self._pin_play self._last_pin_pause = self._pin_pause except Exception as ex: self.log_error(ex) pass ##~~ Softwareupdate hook def get_update_information(self): """ Softwareupdate hook, standard library hook to handle software update and plugin version info """ # Define the configuration for your plugin to use with the Software Update # Plugin here. See https://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html # for details. return dict( display_panel=dict( displayName="OctoPrint Micro Panel", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-DisplayPanel", current=self._plugin_version, # update method: pip pip="https://github.com/sethvoltz/OctoPrint-DisplayPanel/archive/{target_version}.zip" ) ) ##~~ Helpers def bcm2board(self, bcm_pin): """ Function to translate bcm pin to board pin """ board_pin = -1 if bcm_pin != -1: _bcm2board = [ -1, -1, -1, 7, 29, 31, -1, -1, -1, -1, -1, 32, 33, -1, -1, 36, 11, 12, 35, 38, 40, 15, 16, 18, 22, 37, 13 ] board_pin=_bcm2board[bcm_pin-1] return board_pin def start_system_timer(self): """ Function to check system stats periodically """ self._check_system_timer = RepeatedTimer(5, self.check_system_stats, None, None, True) self._check_system_timer.start() def check_system_stats(self): """ Function to collect general system stats about the underlying system(s). Called by the system check timer on a regular basis. This function should remain small, performant and not block. """ if self._screen_mode == ScreenModes.SYSTEM: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) self._system_stats['ip'] = s.getsockname()[0] s.close() self._system_stats['load'] = psutil.getloadavg() self._system_stats['memory'] = psutil.virtual_memory() self._system_stats['disk'] = shutil.disk_usage('/') # disk percentage = 100 * used / (used + free) self.update_ui() elif self._screen_mode == ScreenModes.PRINTER: # Just update the UI, the printer mode will take care of itself self.update_ui() def setup_display(self): """ Intialize display """ try: self.i2c = busio.I2C(SCL, SDA) self.disp = adafruit_ssd1306.SSD1306_I2C(128, 64, self.i2c, addr=int(self._i2c_address,0)) self._logger.info("Setting display to I2C address %s", self._i2c_address) self._display_init = True self.font = ImageFont.load_default() self.width = self.disp.width self.height = self.disp.height self.image = Image.new("1", (self.width, self.height)) self.draw = ImageDraw.Draw(self.image) self._screen_mode = ScreenModes.SYSTEM except Exception as ex: self.log_error(ex) pass def setup_gpio(self): """ Setup GPIO to use BCM pin numbering, unless already setup in which case fall back and update globals to reflect change. """ self.BCM_PINS = { self._pin_mode: 'mode', self._pin_cancel: 'cancel', self._pin_play: 'play', self._pin_pause: 'pause' } self.BOARD_PINS = { self.bcm2board(self._pin_mode) : 'mode', self.bcm2board(self._pin_cancel): 'cancel', self.bcm2board(self._pin_play): 'play', self.bcm2board(self._pin_pause): 'pause' } self.input_pinset = self.BCM_PINS try: current_mode = GPIO.getmode() set_mode = GPIO.BCM if current_mode is None: GPIO.setmode(set_mode) self.input_pinset = self.BCM_PINS self._logger.info("Setting GPIO mode to BCM numbering") elif current_mode != set_mode: GPIO.setmode(current_mode) self.input_pinset = self.BOARD_PINS self._logger.info("GPIO mode was already set, adapting to use BOARD numbering") GPIO.setwarnings(False) self._gpio_init = True except Exception as ex: self.log_error(ex) pass def configure_gpio(self): """ Setup the GPIO pins to handle the buttons as inputs with built-in pull-up resistors """ if self._gpio_init: for gpio_pin in self.input_pinset: self.configure_single_gpio(gpio_pin) def configure_single_gpio(self, gpio_pin): """ Setup the GPIO pins to handle the buttons as inputs with built-in pull-up resistors """ if self._gpio_init: try: if gpio_pin != -1: GPIO.setup(gpio_pin, GPIO.IN, GPIO.PUD_UP) GPIO.remove_event_detect(gpio_pin) self._logger.info("Adding GPIO event detect on pin %s with edge: FALLING", gpio_pin) GPIO.add_event_detect(gpio_pin, GPIO.FALLING, callback=self.handle_gpio_event, bouncetime=self._debounce) except Exception as ex: self.log_error(ex) def clean_gpio(self): """ Remove event detection and clean up for all pins (`mode`, `cancel`, `play` and `pause`) """ if self._gpio_init: for gpio_pin in self.input_pinset: self.clean_single_gpio(gpio_pin) def clean_single_gpio(self, gpio_pin): """ Remove event detection and clean up for all pins (`mode`, `cancel`, `play` and `pause`) """ if self._gpio_init: if gpio_pin!=-1: try: GPIO.remove_event_detect(gpio_pin) except Exception as ex: self.log_error(ex) pass try: GPIO.cleanup(gpio_pin) except Exception as ex: self.log_error(ex) pass self._logger.info("Removed GPIO pin %s", gpio_pin) def handle_gpio_event(self, channel): """ Event callback for GPIO event, called from `add_event_detect` setup in `configure_gpio` """ try: if channel in self.input_pinset: if self._display_timeout_active: self.start_display_timer() return else: self.start_display_timer() label = self.input_pinset[channel] if label == 'cancel': self.try_cancel() else: if self._cancel_timer is not None: self.clear_cancel() else: if label == 'mode': self.next_mode() if label == 'play': self.try_play() if label == 'pause': self.try_pause() except Exception as ex: self.log_error(ex) pass def next_mode(self): """ Go to the next screen mode """ self._screen_mode = self._screen_mode.next() self.update_ui() def try_cancel(self): """ First click, confirm cancellation. Second click, trigger cancel """ if not self._printer.is_printing() and not self._printer.is_paused(): return if self._cancel_timer: # GPIO can double-trigger sometimes, check if this is too fast and ignore if (time.time() - self._cancel_requested_at) < 1: return # Cancel has been tried, run a cancel self.clear_cancel() self._printer.cancel_print() else: # First press self._cancel_requested_at = time.time() self._cancel_timer = RepeatedTimer(10, self.clear_cancel, run_first=False) self._cancel_timer.start() self.update_ui() def clear_cancel(self): """ Clear a pending cancel, something else was clicked or a timeout occured """ if self._cancel_timer: self._cancel_timer.cancel() self._cancel_timer = None self._cancel_requested_at = 0 self.update_ui() def try_play(self): """ If possible, play or resume a print """ # TODO: If a print is queued up and ready, start it if not self._printer.is_paused(): return self._printer.resume_print() def try_pause(self): """ If possible, pause a running print """ if not self._printer.is_printing(): return self._printer.pause_print() def clear_display(self): """ Clear the OLED display completely. Used at startup and shutdown to ensure a blank screen """ if self._display_init: self.disp.fill(0) self.disp.show() def set_printer_state(self, event): """ Set printer state based on latest event """ if event in (Events.DISCONNECTED, Events.CONNECTED, Events.PRINT_STARTED, Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_PAUSED, Events.PRINT_RESUMED): if event == Events.DISCONNECTED: self._printer_state = 0 if event in (Events.CONNECTED, Events.PRINT_FAILED, Events.PRINT_DONE, Events.PRINT_CANCELLED, Events.PRINT_PAUSED): self._printer_state = 1 if event in (Events.PRINT_STARTED, Events.PRINT_RESUMED): self._printer_state = 2 if self._printer_state != self._last_printer_state: self.start_display_timer(True) self._last_printer_state = self._printer_state return def start_display_timer(self, reconfigure=False): """ Start timer for display timeout """ do_reset = False if self._display_timeout_timer is not None: if reconfigure: self.stop_display_timer() else: do_reset = True if do_reset: self._display_timeout_timer.reset() else: if self._printer_state <= self._display_timeout_option: self._display_timeout_timer = ResettableTimer(self._display_timeout_time * 60, self.trigger_display_timeout, [True], None, True) self._display_timeout_timer.start() if self._display_timeout_active: self.trigger_display_timeout(False) return def stop_display_timer(self): """ Stop timer for display timeout """ if self._display_timeout_timer is not None: self._display_timeout_timer.cancel() self._display_timeout_timer = None return def trigger_display_timeout(self, activate): """ Set display off on activate == True and on on activate == False """ self._display_timeout_active = activate if self._display_init: if activate: self.stop_display_timer() self.disp.poweroff() else: self.disp.poweron() return def update_ui(self): """ Update the on-screen UI based on the current screen mode and printer status """ if self._display_init: try: current_data = self._printer.get_current_data() if self._cancel_timer is not None and current_data['state']['flags']['cancelling'] is False: self.update_ui_cancel_confirm() elif self._screen_mode == ScreenModes.SYSTEM: self.update_ui_system() elif self._screen_mode == ScreenModes.PRINTER: self.update_ui_printer() elif self._screen_mode == ScreenModes.PRINT: self.update_ui_print(current_data) self.update_ui_bottom(current_data) # Display image. if self._image_rotate: self.disp.image(self.image.rotate(angle=180)) else: self.disp.image(self.image) self.disp.show() except Exception as ex: self.log_error(ex) def update_ui_cancel_confirm(self): """ Show a confirmation message that a cancel print has been requested """ top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) if self._display_init: try: self.draw.rectangle((0, top, self.width, bottom), fill=0) display_string = "Cancel Print?" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 0), display_string, font=self.font, fill=255) display_string = "Press 'X' to confirm" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 9), display_string, font=self.font, fill=255) display_string = "Press any button or" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 18), display_string, font=self.font, fill=255) display_string = "wait 10 sec to escape" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + offset + 27), display_string, font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_system(self): """ Update three-fourths of the screen with system stats collected by the timed collector """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) # Draw a black filled box to clear the image. self.draw.rectangle((0, top, self.width, bottom), fill=0) try: mem = self._system_stats['memory'] disk = self._system_stats['disk'] # Center IP ip = self._system_stats['ip'] tWidth, tHeight = self.draw.textsize(ip) self.draw.text((left + ((self.width - tWidth) / 2), top + offset + 0), ip, font=self.font, fill=255) # System Stats self.draw.text((left, top + offset + 9), "L: %s, %s, %s" % self._system_stats['load'], font=self.font, fill=255) self.draw.text((left, top + offset + 18), "M: %s/%s MB %s%%" % (int(mem.used/1048576), int(mem.total/1048576), mem.percent), font=self.font, fill=255) self.draw.text((left, top + offset + 27), "D: %s/%s GB %s%%" % (int(disk.used/1073741824), int((disk.used+disk.total)/1073741824), int(10000*disk.used/(disk.used+disk.free))/100), font=self.font, fill=255) except: self.draw.text((left, top + offset + 9), "Gathering System Stats", font=self.font, fill=255) def update_ui_printer(self): """ Update three-fourths of the screen with stats about the printer, such as temperatures """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) try: self.draw.rectangle((0, top, self.width, bottom), fill=0) self.draw.text((left, top + offset + 0), "Printer Temperatures", font=self.font, fill=255) if self._printer.get_current_connection()[0] == "Closed": self.draw.text((left, top + offset + 9), "Head: no printer", font=self.font, fill=255) self.draw.text((left, top + offset + 18), " Bed: no printer", font=self.font, fill=255) else: temperatures = self._printer.get_current_temperatures() tool = temperatures['tool0'] or None bed = temperatures['bed'] or None self.draw.text((left, top + offset + 9), "Head: %s / %s\xb0C" % (tool['actual'], tool['target']), font=self.font, fill=255) self.draw.text((left, top + offset + 18), " Bed: %s / %s\xb0C" % (bed['actual'], bed['target']), font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_print(self, current_data): """ Update three-fourths of the screen with information about the current ongoing print """ if self._display_init: top = (self._colored_strip_height) * int(self._progress_on_top) bottom = self.height - (self._colored_strip_height * int(not self._progress_on_top)) left = 0 offset = self._area_offset * int(self._progress_on_top) try: self.draw.rectangle((0, top, self.width, bottom), fill=0) self.draw.text((left, top + offset + 0), "State: %s" % (self._printer.get_state_string()), font=self.font, fill=255) if current_data['job']['file']['name']: file_name = current_data['job']['file']['name'] self.draw.text((left, top + offset + 9), "File: %s" % (file_name), font=self.font, fill=255) print_time = self._get_time_from_seconds(current_data['progress']['printTime'] or 0) self.draw.text((left, top + offset + 18), "Time: %s" % (print_time), font=self.font, fill=255) filament = current_data['job']['filament']['tool0'] if "tool0" in current_data['job']['filament'] else current_data['job']['filament'] filament_length = self.float_count_formatter((filament['length'] or 0) / 1000, 3) filament_mass = self.float_count_formatter(filament['volume'] or 0, 3) self.draw.text((left, top + offset + 27), "Filament: %sm/%scm3" % (filament_length, filament_mass), font=self.font, fill=255) # Display height if information available from DisplayLayerProgress plugin height = "{:>5.1f}/{:>5.1f}".format(float(self._displaylayerprogress_current_height), float(self._displaylayerprogress_total_height)) layer = "{:>4d}/{:>4d}".format(self._displaylayerprogress_current_layer, self._displaylayerprogress_total_layer) height_text = "" if self._displaylayerprogress_current_height != -1.0 and self._displaylayerprogress_current_layer != -1: height_text = layer + ";" + height elif self._displaylayerprogress_current_layer != -1: height_text = layer elif self._displaylayerprogress_current_height != -1.0: height_text = height self.draw.text((left, top + offset + 36), height_text, font=self.font, fill=255) else: self.draw.text((left, top + offset + 18), "Waiting for file...", font=self.font, fill=255) except Exception as ex: self.log_error(ex) def update_ui_bottom(self, current_data): """ Update one-fourths of the screen with persistent information about the current print """ if self._display_init: top = (self.height - self._colored_strip_height) * int(not self._progress_on_top) bottom = self.height - ((self.height - self._colored_strip_height) * int(self._progress_on_top)) left = 0 try: # Clear area self.draw.rectangle((0, top, self.width, bottom), fill=0) if self._printer.get_current_connection()[0] == "Closed": # Printer isn't connected display_string = "Printer Not Connected" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['paused'] or current_data['state']['flags']['pausing']: # Printer paused display_string = "Paused" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['cancelling']: # Printer paused display_string = "Cancelling" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) elif current_data['state']['flags']['ready'] and (current_data['progress']['completion'] or 0) < 100: # Printer connected, not printing display_string = "Waiting For Job" text_width = self.draw.textsize(display_string, font=self.font)[0] self.draw.text((self.width / 2 - text_width / 2, top + 4), display_string, font=self.font, fill=255) else: percentage = int(current_data['progress']['completion'] or 0) # Calculate progress from time if current_data['progress']['printTime'] and self._timebased_progress: percentage = int((current_data['progress']['printTime'] or 0) / ((current_data['progress']['printTime'] or 0) + current_data['progress']['printTimeLeft']) * 100) time_left = current_data['progress']['printTimeLeft'] or 0 # Progress bar self.draw.rectangle((0, top + 0, self.width - 1, top + 5), fill=0, outline=255, width=1) bar_width = int((self.width - 5) * percentage / 100) self.draw.rectangle((2, top + 2, bar_width, top + 3), fill=255, outline=255, width=1) # Percentage and ETA self.draw.text((0, top + 5), "%s%%" % (percentage), font=self.font, fill=255) eta = time.strftime(self._eta_strftime, time.localtime(time.time() + time_left)) eta_width = self.draw.textsize(eta, font=self.font)[0] self.draw.text((self.width - eta_width, top + 5), eta, font=self.font, fill=255) except Exception as ex: self.log_error(ex) # Taken from tpmullan/OctoPrint-DetailedProgress def _get_time_from_seconds(self, seconds): hours = 0 minutes = 0 if seconds >= 3600: hours = int(seconds / 3600) seconds = seconds % 3600 if seconds >= 60: minutes = int(seconds / 60) seconds = seconds % 60 return self._etl_format.format(**locals()) def float_count_formatter(self, number, max_chars): """ Show decimals up to a max number of characters, then flips over and rounds to integer """ int_part = "%i" % round(number) if len(int_part) >= max_chars - 1: return int_part elif len("%f" % number) <= max_chars: return "%f" % number else: return "{num:0.{width}f}".format(num=number, width=len(int_part) - 1) def log_error(self, ex): """ Helper function for more complete logging on exceptions """ template = "An exception of type {0} occurred on {1}. Arguments: {2!r}" message = template.format(type(ex).__name__, inspect.currentframe().f_code.co_name, ex.args) self._logger.warn(message)
class EnclosurePlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.BlueprintPlugin, octoprint.plugin.EventHandlerPlugin): enclosureSetTemperature=0.0 enclosureCurrentTemperature=0.0 enclosureCurrentHumidity=0.0 def startGPIO(self): self.configureGPIO(self._settings.get_int(["heaterPin"])) self.configureGPIO(self._settings.get_int(["fanPin"])) self.configureGPIO(self._settings.get_int(["lightPin"])) def configureGPIO(self, pin): os.system("gpio -g mode "+str(pin)+" out") os.system("gpio -g write "+str(pin)+" 1") def startTimer(self): self._checkTempTimer = RepeatedTimer(10, self.checkEnclosureTemp, None, None, True) self._checkTempTimer.start() def checkEnclosureTemp(self): stdout = Popen("sudo "+self._settings.get(["getTempScript"])+" "+str(self._settings.get(["dhtModel"]))+" "+str(self._settings.get(["dhtPin"])), shell=True, stdout=PIPE).stdout sTemp = stdout.read() if sTemp.find("Failed") != -1: self._logger.info("Failed to read Temperature") else: self.enclosureCurrentTemperature = float(sTemp) stdout = Popen("sudo "+self._settings.get(["getHumiScript"])+" "+str(self._settings.get(["dhtModel"]))+" "+str(self._settings.get(["dhtPin"])), shell=True, stdout=PIPE).stdout sTemp = stdout.read() if sTemp.find("Failed") != -1: self._logger.info("Failed to read Humidity") else: self.enclosureCurrentHumidity = float(sTemp) self._plugin_manager.send_plugin_message(self._identifier, dict(enclosuretemp=self.enclosureCurrentTemperature,enclosureHumidity=self.enclosureCurrentHumidity)) self.heaterHandler() def heaterHandler(self): command="" if self.enclosureCurrentTemperature<float(self.enclosureSetTemperature) and self._settings.get_boolean(["heaterEnable"]): os.system("gpio -g write "+str(self._settings.get_int(["heaterPin"]))+" 0") else: os.system("gpio -g write "+str(self._settings.get_int(["heaterPin"]))+" 1") os.system(command) #~~ StartupPlugin mixin def on_after_startup(self): self.startTimer() self.startGPIO() #~~ Blueprintplugin mixin @octoprint.plugin.BlueprintPlugin.route("/setEnclosureTemperature", methods=["GET"]) def setEnclosureTemperature(self): self.enclosureSetTemperature = flask.request.values["enclosureSetTemp"] self.heaterHandler() return flask.jsonify(enclosureSetTemperature=self.enclosureSetTemperature,enclosureCurrentTemperature=self.enclosureCurrentTemperature) @octoprint.plugin.BlueprintPlugin.route("/getEnclosureSetTemperature", methods=["GET"]) def getEnclosureSetTemperature(self): return str(self.enclosureSetTemperature) @octoprint.plugin.BlueprintPlugin.route("/getEnclosureTemperature", methods=["GET"]) def getEnclosureTemperature(self): return str(self.enclosureCurrentTemperature) @octoprint.plugin.BlueprintPlugin.route("/handleFan", methods=["GET"]) def handleFan(self): if self._settings.get_boolean(["fanEnable"]): if flask.request.values["status"] == "on": os.system("gpio -g write "+str(self._settings.get_int(["fanPin"]))+" 0") else: os.system("gpio -g write "+str(self._settings.get_int(["fanPin"]))+" 1") return flask.jsonify(success=True) @octoprint.plugin.BlueprintPlugin.route("/handleLight", methods=["GET"]) def handleLight(self): if self._settings.get_boolean(["lightEnable"]): if flask.request.values["status"] == "on": os.system("gpio -g write "+str(self._settings.get_int(["lightPin"]))+" 0") else: os.system("gpio -g write "+str(self._settings.get_int(["lightPin"]))+" 1") return flask.jsonify(success=True) #~~ EventPlugin mixin def on_event(self, event, payload): if event != "PrintDone": return if self._settings.get(['heaterEnable']): self.enclosureSetTemperature = 0 if self._settings.get(['fanEnable']): os.system("gpio -g write "+str(self._settings.get_int(["lightPin"]))+" 1") #~~ SettingsPlugin mixin def on_settings_save(self, data): old_heaterPin = self._settings.get_int(["heaterPin"]) old_dhtPin = self._settings.get_int(["dhtPin"]) old_fanPin = self._settings.get_int(["fanPin"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_heaterPin = self._settings.get_int(["heaterPin"]) new_dhtPin = self._settings.get_int(["dhtPin"]) new_fanPin = self._settings.get_int(["fanPin"]) if new_heaterPin != old_heaterPin: self.configureGPIO(new_heaterPin) if old_dhtPin != new_dhtPin: self.configureGPIO(new_dhtPin) if old_fanPin != new_fanPin: self.configureGPIO(new_fanPin) def get_settings_defaults(self): return dict( heaterEnable=False, heaterPin=18, fanPin=23, lightPin=15, dhtPin=4, dhtModel=22, fanEnable=False, lightEnable=False, getTempScript="~/.octoprint/plugins/OctoPrint-Enclosure/extras/GetTemperature.py", getHumiScript="~/.octoprint/plugins/OctoPrint-Enclosure/extras/GetHumidity.py" ) #~~ TemplatePlugin def get_template_configs(self): return [dict(type="settings", custom_bindings=False)] ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/enclosure.js"] ) ##~~ Softwareupdate hook def get_update_information(self): return dict( enclosure=dict( displayName="Enclosure Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Enclosure", current=self._plugin_version, # update method: pip pip="https://github.com/vitormhenrique/OctoPrint-Enclosure/archive/{target_version}.zip" ) )
class MiTemperature2Plugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.EventHandlerPlugin): ## Requires StartupPlugin def on_after_startup(self): self._logger.info("MiTemperature2 started! (MAC: %s)" % self._settings.get(["mac_address"])) self._start_repeat_timer() ## Requires SettingsPlugin def get_settings_defaults(self): # default settings here return dict( # config command=str(pathlib.Path(__file__).parent.absolute()) + "/MiTemperature2/LYWSD03MMC.py", args="-r -b -c 1 --callback sendToFile.sh", mac_address="A4:C1:38:2D:86:49", seconds="60", # values from device temp="0", humidity="0", bat_voltage="0", bat_level="0") ## Requires SettingsPlugin # def on_settings_save(self, data): # # default save function # octoprint.plugin.SettingsPlugin.on_settings_save(self, data) # # refresh cached settings # self._cachedSettings.updateSettings(self._settings) # self._update_device_data("Saved values") ## Requires SettingsPlugin # def on_settings_load(self): # return dict(octoprint.plugin.SettingsPlugin.on_settings_load(self)) ## Requires TemplatePlugin def get_template_configs(self): return [ dict(type="navbar", template="mitemp2_navbar.jinja2", custom_bindings=True), dict(type="settings", template="mitemp2_settings.jinja2", custom_bindings=True, name="Xiaomi MiTemperature2") ] ## SoftwareUpdate hook def get_update_information(self, *args, **kwargs): return dict(mitemp2=dict( displayName=self._plugin_name, displayVersion=self._plugin_version, # version check: github repository type="github_release", current=self._plugin_version, user="******", repo="octoprint_mitemp2", # update method: pip pip= "https://github.com/shawe/octoprint-mitemp2/archive/{target}.zip")) def _get_command_reply(self): cmd = self._settings.get(["command"]) args = " " + self._settings.get( ["args"]) + " -d " + self._settings.get(["mac_address"]) proc = subprocess.Popen( cmd + " " + args, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) msg = 'through stdin to stdout\n'.encode('utf-8') stdout_value, stderr_value = proc.communicate(msg) output = repr(stdout_value.decode('utf-8')) output = output.split("\n") output = output[0].split('\\') res = [] for line in output: res.append(line) return res def _get_device_values(self): res = self._get_command_reply() results = [] for i in range(1, len(res) - 1): line = res[i] line = line.split(' ') results.append(line) res = { "temperature": results[0][1], "humidity": results[1][1], "bat_voltage": results[2][2], "bat_level": results[3][2] } return res def _update_device_details(self): reply = self._get_device_values() self._logger.info("Nuevos datos: %s" % str(reply)) return reply def _update_device_data(self, updateReason=""): update = self._update_device_details() # store new values self._settings.set(["temperature"], update["temperature"]) self._settings.set(["humidity"], update["humidity"]) self._settings.set(["bat_voltage"], update["bat_voltage"]) self._settings.set(["bat_level"], update["bat_level"]) # prepare clientMessage clientMessageDict = dict() currentValueDict = { "[mac_address]": self._settings.get(["mac_address"]), "[temperature]": self._settings.get(["temperature"]), "[humidity]": self._settings.get(["humidity"]), "[bat_voltage]": self._settings.get(["bat_voltage"]), "[bat_level]": self._settings.get(["bat_level"]) } navBarMessagePattern = "MiTemp2 <span>[mac_address]</span> <span>[temperature] ºC</span> <span>[humidity] %</span> <span>[bat_voltage] V</span> <span>([bat_level] %)</span>" navBarMessage = stringUtils.multiple_replace(navBarMessagePattern, currentValueDict) clientMessageDict.update({'navBarMessage': navBarMessage}) self._plugin_manager.send_plugin_message(self._identifier, clientMessageDict) def _interval(self): return int(self._settings.get(["seconds"])) def _start_repeat_timer(self): self._repeat_timer = RepeatedTimer( self._interval(), self._update_device_data("Repeat timer")) self._repeat_timer.start() self._logger.info("MiTemperature2 every %s seconds" % str(self._interval()))
class OctoPiBoxPlugin(octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin): def get_settings_defaults(self): return dict( enabled=False, timer_seconds=600, printer_pin=17, spare_pin=4, printer_button_pin=6, printer_button_enable_pin=18, spare_button_pin=5, spare_button_enable_pin=27, button_debounce=200, status_red_pin=22, status_green_pin=23, status_blue_pin=24 ) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._load_settings() self._octopibox.restart( self._printer_pin, self._spare_pin, self._printer_button_pin, self._spare_button_pin, self._button_debounce) self._octopibox.restart_status_LED(self._status_red_pin, self._status_green_pin, self._status_blue_pin) def on_after_startup(self): self._timeout_value = None self._timer = None self._load_settings() self._octopibox = OctoPiBoxControl(self, self._printer_pin, self._spare_pin, self._printer_button_pin, self._spare_button_pin, self._button_debounce, self._powercallbackfunction) self._update_power_status() self._octopibox.pin_on(self._printer_button_enable_pin) self._octopibox.pin_on(self._spare_button_enable_pin) self._octopibox.init_status_LED(self._status_red_pin, self._status_green_pin, self._status_blue_pin) self._set_status_LED("DISCONNECTED") def on_shutdown(self): self._octopibox.pin_off(self._printer_button_enable_pin) self._octopibox.pin_off(self._spare_button_enable_pin) self._octopibox.clear_status_LED() self._octopibox.cancel() def get_assets(self): return dict(js=["js/octopibox.js"]) def get_template_configs(self): return [ dict(type="settings", name="OctoPiBox Configuration", custom_bindings=False) ] def get_api_commands(self): return dict(autopoweroff=[], abort=[]) def on_api_command(self, command, data): import flask if command == "abort": self._timer.cancel() self._timer = None self._set_status_LED("CONNECTED") self._logger.info("Automatic Power-Off aborted.") elif command == "autopoweroff": self._logger.debug("Automatic Printer Power-off called via API. Starting timer.") self._start_auto_power_off_timer() def on_event(self, event, payload): #self._logger.info("Event triggered: {}".format(event)) if event == Events.PRINT_DONE: self._octopibox.pin_on(self._printer_button_enable_pin) if not self._enabled: self._logger.debug("Print complete. Automatic Printer Power-off is currently DISABLED.") self._set_status_LED("CONNECTED") return if self._timer is not None: return self._logger.debug("Print complete. Automatic Printer Power-off is ENABLED. Starting timer.") self._start_auto_power_off_timer() elif event == Events.CONNECTED: self._set_status_LED("CONNECTED") elif event == Events.DISCONNECTED: self._set_status_LED("DISCONNECTED") elif event == Events.PRINT_STARTED: self._octopibox.pin_off(self._printer_button_enable_pin) self._set_status_LED("PRINTING") elif event == Events.PRINT_FAILED: self._octopibox.pin_on(self._printer_button_enable_pin) self._set_status_LED("ERROR") elif event == Events.PRINT_CANCELLED: self._octopibox.pin_on(self._printer_button_enable_pin) self._set_status_LED("ERROR") elif event == Events.CLIENT_OPENED: self._update_power_status() def _start_auto_power_off_timer(self): self._timeout_value = self._settings.get_int(['timer_seconds']) if (self._timeout_value < 30) | (self._timeout_value > 1800): self._timeout_value = 600 self._logger.debug("Automatic Printer Power-off started: {} seconds.".format(self._timeout_value)) self._set_status_LED("POWERINGOFF") self._timer = RepeatedTimer(1, self._timer_task) self._timer.start() self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeout", timeout_value=self._timeout_value)) def _load_settings(self): self._enabled = self._settings.get_boolean(['enabled']) self._printer_pin = self._settings.get_int(['printer_pin']) self._spare_pin = self._settings.get_int(['spare_pin']) self._printer_button_pin = self._settings.get_int(['printer_button_pin']) self._printer_button_enable_pin = self._settings.get_int(['printer_button_enable_pin']) self._spare_button_pin = self._settings.get_int(['spare_button_pin']) self._spare_button_enable_pin = self._settings.get_int(['spare_button_enable_pin']) self._button_debounce = self._settings.get_int(['button_debounce']) self._status_red_pin = self._settings.get_int(['status_red_pin']) self._status_green_pin = self._settings.get_int(['status_green_pin']) self._status_blue_pin = self._settings.get_int(['status_blue_pin']) def _timer_task(self): self._timeout_value -= 1 self._plugin_manager.send_plugin_message(self._identifier, dict(type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: self._timer.cancel() self._timer = None self._printeroff() def _powercallbackfunction(self, pin, level, tick): if pin == self._printer_pin: self._logger.debug("Printer pin {} level changed to {}".format(pin, level)) self._update_power_status() if level == 0: current_connection = self._printer.get_current_connection() if current_connection[0] != "Closed": self._logger.debug("Printer connection found: {}".format(current_connection[0:3])) self._printer.disconnect() self._logger.debug("Printer disconnected after power-off.") if self._timeout_value > 0: self._timer.cancel() self._timer = None self._timeout_value = 0 self._plugin_manager.send_plugin_message(self._identifier, dict(type="close_popup")) elif level == 1: self._logger.debug("Printer power-on detected.") self._set_status_LED("CONNECTING") self._printer.connect() self._logger.debug("Printer auto-connect after power-on attempted.") def _printeroff(self): self._logger.debug("Printer disconnect before power-off.") self._printer.disconnect() self._logger.info("Powering off printer on pin {}.".format( self._printer_pin)) self._octopibox.pin_off(self._printer_pin) self._logger.debug("Powering off spare outlet on pin {}.".format( self._spare_pin)) self._octopibox.pin_off(self._spare_pin) def _update_power_status(self): printer_power_status = ["Off", "On"] printer_power_status_text = printer_power_status[ self._octopibox.pin_value(self._printer_pin)] self._plugin_manager.send_plugin_message(self._identifier, dict(type="updatePowerStatus", power_status_value=printer_power_status_text)) self._logger.debug("Data message sent from {} for power update to {}.".format(self._identifier, printer_power_status_text)) def _set_status_LED(self, status="DISCONNECTED"): self._octopibox.clear_status_LED() if status=="DISCONNECTED": self._octopibox.set_status_LED_color("YELLOW", "OFF", "OFF") elif status == "CONNECTED": self._octopibox.set_status_LED_color("GREEN", "OFF", "OFF") elif status == "PRINTING": self._octopibox.set_status_LED_color("RED", "OFF", "OFF") elif status == "CONNECTING": self._octopibox.set_status_LED_color("GREEN", "YELLOW", "SLOW") elif status == "POWERINGOFF": self._octopibox.set_status_LED_color("RED", "YELLOW", "SLOW") elif status == "ERROR": self._octopibox.set_status_LED_color("RED", "OFF", "FAST") elif status == "OFF": self._octopibox.set_status_LED_color("OFF", "OFF", "OFF") else: self._octopibox.set_status_LED_color("OFF", "OFF", "OFF") ##~~ 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( octopibox=dict( displayName="OctoPiBox Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-OctoPiBox", current=self._plugin_version, # update method: pip pip="https://github.com/hcomet/OctoPrint-OctoPiBox/archive/{target_version}.zip" ) )
class PersistConnectionPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): self._timer = None self._enabled = None self._firmware_plugin = None self._automation_plugin = None def get_settings_defaults(self): return dict( enabled=True ) def get_api_commands(self): return dict( toggle_enabled=[] ) def on_after_startup(self): self._firmware_plugin = self._plugin_manager.get_plugin_info( "firmwareupdate") self._automation_plugin = self._plugin_manager.get_plugin_info( "automation_scripts") self._enabled = self._settings.get_boolean(["enabled"]) self._timer = RepeatedTimer( 1.0, self._check_connection, run_first=True) self._timer.start() def on_api_command(self, command, data): if command == "toggle_enabled": if data['current']: self._enabled = False else: self._enabled = True self._settings.set_boolean(["enabled"], self._enabled) self._settings.save() eventManager().fire(Events.SETTINGS_UPDATED) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._enabled = self._settings.get_boolean(["enabled"]) def _check_connection(self): state = self._printer.get_state_string() if self._firmware_plugin is None: firmware_updating = False else: firmware_updating = ( self._firmware_plugin.implementation._is_updating()) if self._automation_plugin is None: automation_running = False else: automation_running = ( self._automation_plugin.implementation._is_running()) if self._enabled and not firmware_updating and not automation_running: if state in ["Closed", "Offline"]: self._logger.info("Offline or closed; reconnecting") self._printer.connect() elif "Error" in state or state == "Unknown": self._logger.info("Error detected; reopening connection") self._printer.disconnect() self._printer.connect() sleep(15)
class PowerinfoPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SimpleApiPlugin): ##~~ Intialization def __init__(self): self._checkPwrStatusTimer = None self.inOnePin = "" self.inTwoPin = "" self.isRaspi = False self.pwrOnName = "" self.pwrOffName = "" self.pwrOneStatus = "" self.pwrTwoStatus = "" self.relayOneName = "" self.relayTwoName = "" self.rOneMessage = "" self.rRate = "" self.rTwoMessage = "" self.showPwrOneStatus = False self.showPwrTwoStatus = False ##~~ StartupPlugin mixin def on_after_startup(self): # Get our settings self.inOnePin = int(self._settings.get(["inOnePin"])) self.inTwoPin = int(self._settings.get(["inTwoPin"])) self.pwrOnName = self._settings.get(["pwrOnName"]) self.pwrOffName = self._settings.get(["pwrOffName"]) self.relayOneName = self._settings.get(["relayOneName"]) self.relayTwoName = self._settings.get(["relayTwoName"]) self.rRate = int(self._settings.get(["rRate"])) self.showPwrOneStatus = self._settings.get(["showPwrOneStatus"]) self.showPwrTwoStatus = self._settings.get(["showPwrTwoStatus"]) # Update the plugin helpers __plugin_helpers__.update(dict( inOnePin=self.inOnePin, inTwoPin=self.inTwoPin, relayOneName=self.relayOneName, relayTwoName=self.relayTwoName, showPwrOneRelay=self.showPwrOneStatus, showPwrTwoRelay=self.showPwrTwoStatus )) self._logger.info("Powerinfo plugin started") if sys.platform == "linux2": with open('/proc/cpuinfo', 'r') as infile: cpuinfo = infile.read() # Search for the cpu info match = re.search('^Hardware\s+:\s+(\w+)$', cpuinfo, flags=re.MULTILINE | re.IGNORECASE) if match is None: # The hardware is not a pi. self.isRaspi = False elif match.group(1) == 'BCM2708': self._logger.debug("Pi 1") self.isRaspi = True elif match.group(1) == 'BCM2709': self._logger.debug("Pi 2") self.isRaspi = True elif match.group(1) == 'BCM2710': self._logger.debug("Pi 3") self.isRaspi = True if self.showPwrOneStatus or self.showPwrTwoStatus and self.isRaspi: self._logger.debug("Initialize GPOI") # Set GPIO layout like pin-number GPIO.setmode(GPIO.BOARD) # Configure our GPIO outputs GPIO.setup(self.inOnePin, GPIO.OUT) GPIO.setup(self.inTwoPin, GPIO.OUT) # Setup the initial state to high(off) GPIO.output(self.inOnePin, GPIO.HIGH) GPIO.output(self.inTwoPin, GPIO.HIGH) self._logger.debug("Start the timer now") self.startTimer(self.rRate) self._logger.debug("Running on Pi? - %s" % self.isRaspi) def startTimer(self, interval): self._checkPwrStatusTimer = RepeatedTimer(interval, self.checkPwrStatus, None, None, True) self._checkPwrStatusTimer.start() def checkPwrStatus(self): # Check the GPIO status of our relays and set our message self.pwrOneStatus = GPIO.input(self.inOnePin) if self.pwrOneStatus: self.rOneMessage = "%s: %s" % (self.relayOneName, self.pwrOffName) else: self.rOneMessage = "%s: %s" % (self.relayOneName, self.pwrOnName) self.pwrTwoStatus = GPIO.input(self.inTwoPin) if self.pwrTwoStatus: self.rTwoMessage = "%s: %s" % (self.relayTwoName, self.pwrOffName) else: self.rTwoMessage = "%s: %s" % (self.relayTwoName, self.pwrOnName) # Send our status message self._plugin_manager.send_plugin_message(self._identifier, dict(messageOne=self.rOneMessage, messageOneShow=self.showPwrOneStatus, messageTwo=self.rTwoMessage, messageTwoShow=self.showPwrTwoStatus)) ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( inOnePin="11", inTwoPin="12", pwrOnName="On", pwrOffName="Off", relayOneName="Printer", relayTwoName="Light", rRate="10", showPwrOneStatus=True, showPwrTwoStatus=False ) def on_settings_save(self,data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.inOnePin = int(self._settings.get(["inOnePin"])) self.inTwoPin = int(self._settings.get(["inTwoPin"])) self.pwrOnName = self._settings.get(["pwrOnName"]) self.pwrOffName = self._settings.get(["pwrOffName"]) self.relayOneName = self._settings.get(["relayOneName"]) self.relayTwoName = self._settings.get(["relayTwoName"]) self.rRate = int(self._settings.get(["rRate"])) self.showPwrOneStatus = self._settings.get(["showPwrOneStatus"]) self.showPwrTwoStatus = self._settings.get(["showPwrTwoStatus"]) # Update the plugin helpers __plugin_helpers__.update(dict( inOnePin=self.inOnePin, inTwoPin=self.inTwoPin, relayOneName=self.relayOneName, relayTwoName=self.relayTwoName, showPwrOneRelay=self.showPwrOneStatus, showPwrTwoRelay=self.showPwrTwoStatus )) if self.showPwrOneStatus or self.showPwrTwoStatus and self.isRaspi: # Initialize the GPIO after a setting change GPIO.cleanup() # Set GPIO layout like pin-number GPIO.setmode(GPIO.BOARD) # Configure our GPIO outputs GPIO.setup(self.inOnePin, GPIO.OUT) GPIO.setup(self.inTwoPin, GPIO.OUT) # Setup the initial state to high(off) GPIO.output(self.inOnePin, GPIO.HIGH) GPIO.output(self.inTwoPin, GPIO.HIGH) # Start the timer self.startTimer(self.rRate) else: if self._checkPwrStatusTimer is not None: try: self._checkPwrStatusTimer.cancel() except: pass self._plugin_manager.send_plugin_message(self._identifier, dict()) ##~~ AssetPlugin mixin def get_assets(self): return dict( js=["js/powerinfo.js"] ) ##~~ TemplatePlugin mixin def get_template_configs(self): if self.isRaspi: return [ dict(type="sidebar", name="Powerinfo", icon="info"), dict(type="settings", name="Powerinfo", custom_bindings=False) ] else: return [ ] ##~~ SimpleApiPlugin mixin def on_api_get(self, request): return flask.jsonify(dict( messageOne=self.rOneMessage, messageOneShow=self.showPwrOneStatus, messageTwo=self.rTwoMessage, messageTwoShow=self.showPwrTwoStatus )) ##~~ Softwareupdate hook def get_update_information(self): return dict( powerinfo=dict( displayName="Powerinfo Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-Powerinfo", current=self._plugin_version, # update method: pip pip="https://github.com/konvader/OctoPrint-Powerinfo/archive/{target_version}.zip" ) )
class PowerFailurePlugin(octoprint.plugin.TemplatePlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin): def __init__(self): super(PowerFailurePlugin, self).__init__() self.will_print = "" def get_settings_defaults(self): return dict( auto_continue=False, z_homing_height=0, gcode=("M80\n" "M140 S{bedT}\n" "M104 S{tool0T}\n" "M190 S{bedT}\n" "M109 S{tool0T}\n" "G21 ;metric values\n" "G90 ;absolute positioning\n" "G28 X0 Y0 ;move X/Y to min endstops\n" "G92 E0 Z{currentZ} ;zero the extruded length again\n" "M211 S0\n" "G91\n" "G1 Z-{z_homing_height} F200 ; correcting Z_HOMING_HEIGHT\n" "G90\n" "M211 S1\n" "G1 F9000\n"), recovery=False, filename="", filepos=0, currentZ=0.0, bedT=0.0, tool0T=0.0) def on_after_startup(self): if self._settings.getBoolean(["recovery"]): # hay que recuperar self._logger.info("Recovering from a power failure") filename = self._settings.get(["filename"]) filepos = self._settings.getInt(["filepos"]) currentZ = self._settings.getFloat(["currentZ"]) bedT = self._settings.getFloat(["bedT"]) tool0T = self._settings.getFloat(["tool0T"]) self._logger.info("Recovering printing of %s" % filename) recovery_fn = self.generateContinuation(filename, filepos, currentZ, bedT, tool0T) self.clean() if self._settings.getBoolean(["auto_continue"]): self.will_print = recovery_fn self._printer.select_file( recovery_fn, False, printAfterSelect=False) # selecciona directo self._logger.info("Recovered from a power failure") else: self._logger.info("There was no power failure.") def generateContinuation(self, filename, filepos, currentZ, bedT, tool0T): z_homing_height = self._settings.getFloat(["z_homing_height"]) currentZ += z_homing_height gcode = self._settings.get(["gcode"]).format(**locals()) original_fn = self._file_manager.path_on_disk("local", filename) path, filename = os.path.split(original_fn) recovery_fn = self._file_manager.path_on_disk( "local", os.path.join(path, "recovery_" + filename)) fan = False extruder = False for line in reverse_readlines(original_fn, filepos): # buscando las ultimas lineas importantes if not fan and (line.startswith("M106") or line.startswith("M107")): fan = True # encontre el fan gcode += line + "\n" if not extruder and (line.startswith("G1 ") or line.startswith("G92 ")) and ("E" in line): # G1 X135.248 Y122.666 E4.03755 extruder = True # encontre el extruder subcommands = line.split() # dividido por espacios ecommand = [sc for sc in subcommands if "E" in sc] assert len(ecommand) == 1 ecommand = ecommand[0] gcode += "G92 " + ecommand + "\n" if fan and extruder: break original = open(original_fn, 'r') original.seek(filepos) data = gcode + original.read() original.close() stream = octoprint.filemanager.util.StreamWrapper( recovery_fn, io.BytesIO(data)) self._file_manager.add_file( octoprint.filemanager.FileDestinations.LOCAL, recovery_fn, stream, allow_overwrite=True) return os.path.join(path, "recovery_" + filename) def get_template_configs(self): return [dict(type="settings", custom_bindings=False)] def backupState(self): currentData = self._printer.get_current_data() if currentData["job"]["file"]["origin"] != "local": self._logger.info( "SD printing does not support power failure recovery") self._settings.setBoolean(["recovery"], False) self.timer.cancel() return currentTemp = self._printer.get_current_temperatures() bedT = currentTemp["bed"]["target"] tool0T = currentTemp["tool0"]["target"] filepos = currentData["progress"]["filepos"] filename = currentData["job"]["file"]["path"] currentZ = currentData["currentZ"] self._logger.info("Backup printing: %s Offset:%s Z:%s Bed:%s Tool:%s" % (filename, filepos, currentZ, bedT, tool0T)) self._settings.setBoolean(["recovery"], True) self._settings.set(["filename"], str(filename)) self._settings.setInt(["filepos"], sanitize_number(filepos)) self._settings.setFloat(["currentZ"], sanitize_number(currentZ)) self._settings.setFloat(["bedT"], sanitize_number(bedT)) self._settings.setFloat(["tool0T"], sanitize_number(tool0T)) self._settings.save() def clean(self): self._settings.setBoolean(["recovery"], False) self._settings.save() def on_event(self, event, payload): if self.will_print and self._printer.is_ready(): will_print, self.will_print = self.will_print, "" # larga imprimiendo directamente self._printer.select_file(will_print, False, printAfterSelect=True) if event.startswith("Print"): if event in {"PrintStarted"}: # empiezo a revisar # empiezo a chequear self.timer = RepeatedTimer( 1.0, PowerFailurePlugin.backupState, args=[self], run_first=True, ) self.timer.start() # casos en que dejo de revisar y borro elif event in {"PrintDone", "PrintFailed", "PrintCancelled"}: # cancelo el chequeo self.timer.cancel() self.clean() else: # casos pause y resume pass def get_update_information(self): return dict(powerfailure=dict( displayName=self._plugin_name, displayVersion=self._plugin_version, type="github_release", current=self._plugin_version, user="******", repo="Octoprint-PowerFailure", pip= "https://github.com/pablogventura/OctoPrint-PowerFailure/archive/{target_version}.zip" ))
class TrackingPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.EnvironmentDetectionPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.ShutdownPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.WizardPlugin, octoprint.plugin.EventHandlerPlugin): def __init__(self): self._environment = None self._throttle_state = None self._helpers_get_throttle_state = None self._printer_connection_parameters = None self._url = None self._ping_worker = None self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) self._record_next_firmware_info = False def initialize(self): self._init_tracking() ##~~ SettingsPlugin def get_settings_defaults(self): return dict(enabled=None, unique_id=None, server=TRACKING_URL, ping=15*60, events=dict(startup=True, printjob=True, plugin=True, update=True, printer=True, throttled=True)) def get_settings_restricted_paths(self): return dict(admin=[["enabled"], ["unique_id"], ["events"]], never=[["server"], ["ping"]]) def on_settings_save(self, data): enabled = self._settings.get([b"enabled"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) if enabled is None and self._settings.get([b"enabled"]): # tracking was just enabled, let's init it and send a startup event self._init_tracking() self._track_startup() ##~~ EnvironmentDetectionPlugin def on_environment_detected(self, environment, *args, **kwargs): self._environment = environment ##~~ StartupPlugin def on_after_startup(self): ping = self._settings.get_int(["ping"]) if ping: self._ping_worker = RepeatedTimer(ping, self._track_ping) self._ping_worker.start() # cautiously look for the get_throttled helper from pi_support pi_helper = self._plugin_manager.get_helpers("pi_support", "get_throttled") if pi_helper and 'get_throttled' in pi_helper: self._helpers_get_throttle_state = pi_helper['get_throttled'] # now that we have everything set up, phone home. self._track_startup() ##~~ ShutdownPlugin def on_shutdown(self): self._track_shutdown() ##~~ EventHandlerPlugin def on_event(self, event, payload): if event.startswith("plugin_pluginmanager_"): self._track_plugin_event(event, payload) elif event.startswith("plugin_softwareupdate_"): self._track_update_event(event, payload) elif event in ("plugin_pi_support_throttle_state",): self._throttle_state = payload self._track_throttle_event(event, payload) elif event in (Events.PRINT_STARTED, Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED): self._track_printjob_event(event, payload) elif event in (Events.CONNECTED,): self._printer_connection_parameters = dict(port=payload["port"], baudrate=payload["baudrate"]) self._record_next_firmware_info = True elif event in (Events.FIRMWARE_DATA,) and self._record_next_firmware_info: self._record_next_firmware_info = False self._track_printer_event(event, payload) ##~~ TemplatePlugin def get_template_configs(self): return [ dict(type="settings", name=gettext("Anonymous Usage Tracking"), template="tracking_settings.jinja2", custom_bindings=False), dict(type="wizard", name=gettext("Anonymous Usage Tracking"), template="tracking_wizard.jinja2", custom_bindings=True, mandatory=True) ] ##~~ AssetPlugin def get_assets(self): return dict(js=["js/usage.js"]) ##~~ WizardPlugin def is_wizard_required(self): return self._settings.get([b"enabled"]) is None ##~~ helpers def _init_tracking(self): if not self._settings.get_boolean([b"enabled"]): return self._init_id() self._logger.info("Initialized anonymous tracking") def _init_id(self): if self._settings.get_boolean([b"enabled"]) and not self._settings.get([b"unique_id"]): import uuid self._settings.set([b"unique_id"], str(uuid.uuid4())) self._settings.save() def _track_ping(self): self._track("ping") def _track_startup(self): if not self._settings.get_boolean(["events", "startup"]): return payload = dict(version=get_octoprint_version_string(), os=self._environment[b"os"][b"id"], python=self._environment[b"python"][b"version"], pip=self._environment[b"python"][b"pip"], cores=self._environment[b"hardware"][b"cores"], freq=self._environment[b"hardware"][b"freq"], ram=self._environment[b"hardware"][b"ram"]) if b"plugins" in self._environment and b"pi_support" in self._environment[b"plugins"]: payload[b"pi_model"] = self._environment[b"plugins"][b"pi_support"][b"model"] if b"octopi_version" in self._environment[b"plugins"][b"pi_support"]: payload[b"octopi_version"] = self._environment[b"plugins"][b"pi_support"][b"octopi_version"] self._track("startup", **payload) def _track_shutdown(self): if not self._settings.get_boolean(["events", "startup"]): return self._track("shutdown") def _track_plugin_event(self, event, payload): if not self._settings.get_boolean(["events", "plugin"]): return if event.endswith("_installplugin"): self._track("install_plugin", plugin=payload.get(b"id"), plugin_version=payload.get(b"version")) elif event.endswith("_uninstallplugin"): self._track("uninstall_plugin", plugin=payload.get(b"id"), plugin_version=payload.get(b"version")) elif event.endswith("_enableplugin"): self._track("enable_plugin", plugin=payload.get(b"id"), plugin_version=payload.get(b"version")) elif event.endswith("_disableplugin"): self._track("disable_plugin", plugin=payload.get(b"id"), plugin_version=payload.get(b"version")) def _track_update_event(self, event, payload): if not self._settings.get_boolean(["events", "update"]): return if event.endswith("_update_succeeded"): self._track("update_successful", target=payload.get("target"), from_version=payload.get("from_version"), to_version=payload.get("to_version")) elif event.endswith("_update_failed"): self._track("update_failed", target=payload.get("target"), from_version=payload.get("from_version"), to_version=payload.get("to_version")) def _track_throttle_event(self, event, payload): if not self._settings.get_boolean(["events", "throttled"]): return args = dict(throttled_now=payload[b"current_issue"], throttled_past=payload[b"past_issue"], throttled_mask=payload[b"raw_value"], throttled_voltage_now=payload[b"current_undervoltage"], throttled_voltage_past=payload[b"past_undervoltage"], throttled_overheat_now=payload[b"current_overheat"], throttled_overheat_past=payload[b"past_overheat"]) if payload[b"current_issue"]: track_event = "system_throttled" else: track_event = "system_unthrottled" if track_event is not None: self._track(track_event, **args) def _track_printjob_event(self, event, payload): if not self._settings.get_boolean(["events", "printjob"]): return sha = hashlib.sha1() sha.update(payload.get("path")) sha.update(self._settings.get([b"unique_id"])) track_event = None args = dict(origin=payload.get(b"origin"), file=sha.hexdigest()) if event == Events.PRINT_STARTED: track_event = "print_started" elif event == Events.PRINT_DONE: try: elapsed = int(payload.get(b"time")) except ValueError: elapsed = "unknown" args[b"elapsed"] = elapsed track_event = "print_done" elif event == Events.PRINT_FAILED: try: elapsed = int(payload.get(b"time")) except ValueError: elapsed = "unknown" args[b"elapsed"] = elapsed args[b"reason"] = payload.get(b"reason", "unknown") track_event = "print_failed" elif event == Events.PRINT_CANCELLED: track_event = "print_cancelled" if callable(self._helpers_get_throttle_state): try: throttle_state = self._helpers_get_throttle_state(run_now=True) if throttle_state and (throttle_state.get(b"current_issue", False) or throttle_state.get(b"past_issue", False)): args[b"throttled_now"] = throttle_state[b"current_issue"] args[b"throttled_past"] = throttle_state[b"past_issue"] args[b"throttled_mask"] = throttle_state[b"raw_value"] except: # ignored pass if track_event is not None: self._track(track_event, **args) def _track_printer_event(self, event, payload): if not self._settings.get_boolean(["events", "printer"]): return if event in (Events.FIRMWARE_DATA,): args = dict(firmware_name=payload["name"]) if self._printer_connection_parameters: args["printer_port"] = self._printer_connection_parameters["port"] args["printer_baudrate"] = self._printer_connection_parameters["baudrate"] self._track("printer_connected", **args) def _track(self, event, **kwargs): if not self._settings.get_boolean([b"enabled"]): return self._executor.submit(self._do_track, event, **kwargs) def _do_track(self, event, **kwargs): if not self._connectivity_checker.online: return server = self._settings.get([b"server"]) url = server.format(id=self._settings.get([b"unique_id"]), event=event) # Don't print the URL or UUID! That would expose the UUID in forums/tickets # if pasted. It's okay for the user to know their uuid, but it shouldn't be shared. headers = {"User-Agent": "OctoPrint/{}".format(get_octoprint_version_string())} try: params = urlencode(kwargs, doseq=True).replace("+", "%20") requests.get(url, params=params, timeout=3.1, headers=headers) self._logger.info("Sent tracking event {}, payload: {!r}".format(event, kwargs)) except: if self._logger.isEnabledFor(logging.DEBUG): self._logger.exception("Error while sending event to anonymous usage tracking".format(url)) else: pass
class BeeCom(MachineCom): STATE_WAITING_FOR_BTF = 21 STATE_PREPARING_PRINT = 22 STATE_HEATING = 23 STATE_SHUTDOWN = 24 _beeConn = None _beeCommands = None _responseQueue = queue.Queue() _statusQueue = queue.Queue() _monitor_print_progress = True _connection_monitor_active = True _prepare_print_thread = None _preparing_print = False _resume_print_thread = None def __init__(self, callbackObject=None, printerProfileManager=None): super(BeeCom, self).__init__(None, None, callbackObject, printerProfileManager) self._openConnection() # monitoring thread self._monitoring_active = True self.monitoring_thread = threading.Thread(target=self._monitor, name="comm._monitor") self.monitoring_thread.daemon = True self.monitoring_thread.start() def _openConnection(self): """ Opens a new connection using the BEEcom driver :return: True if the connection was successful """ if self._beeConn is None: self._beeConn = BeePrinterConn(self._connShutdownHook) self._changeState(self.STATE_CONNECTING) self._beeConn.connectToFirstPrinter() if self._beeConn.isConnected(): self._beeCommands = self._beeConn.getCommandIntf() # change to firmware if self._beeCommands.getPrinterMode() == 'Bootloader': # checks for firmware updates self.update_firmware() self._beeCommands.goToFirmware() # restart connection self._beeConn.reconnect() # post connection callback self._onConnected() return True else: return False def current_firmware(self): """ Gets the current firmware version :return: """ firmware_v = self.getCommandsInterface().getFirmwareVersion() if firmware_v is not None: return firmware_v else: return 'Not available' def update_firmware(self): """ Updates the printer firmware if the value in the firmware.properties file is different from the current printer firmware :return: if no printer is connected just returns void """ _logger = logging.getLogger() # get the latest firmware file for the connected printer conn_printer = self.getConnectedPrinterName() if conn_printer is None: return printer_id = conn_printer.replace(' ', '').lower() if printer_id: from os.path import isfile, join _logger.info("Checking for firmware updates...") try: firmware_path = settings().getBaseFolder('firmware') firmware_properties = parsePropertiesFile(join(firmware_path, 'firmware.properties')) firmware_file_name = firmware_properties['firmware.' + printer_id] except KeyError as e: _logger.error("Problem with printer_id %s. Firmware properties not found for this printer model." % printer_id) return if firmware_file_name is not None and isfile(join(firmware_path, firmware_file_name)): fname_parts = firmware_file_name.split('-') # gets the current firmware version curr_firmware = self.current_firmware() curr_firmware_parts = curr_firmware.split('-') if len(curr_firmware_parts) == 3 and curr_firmware is not "Not available": curr_version_parts = curr_firmware_parts[2].split('.') file_version_parts = fname_parts[2].split('.') if len(curr_version_parts) >= 3 and len(file_version_parts) >=3: for i in xrange(3): if int(file_version_parts[i]) != int(curr_version_parts[i]): # version update found _logger.info("Updating printer firmware...") self.getCommandsInterface().flashFirmware(join(firmware_path, firmware_file_name), firmware_file_name) _logger.info("Firmware updated to %s" % fname_parts[2]) return elif curr_firmware == '0.0.0': # If curr_firmware is 0.0.0 it means something went wrong with a previous firmware update _logger.info("Updating printer firmware...") self.getCommandsInterface().flashFirmware(join(firmware_path, firmware_file_name), firmware_file_name) _logger.info("Firmware updated to %s" % fname_parts[2]) return else: _logger.error("No firmware file matching the configuration for printer %s found" % conn_printer) _logger.info("No firmware updates found") def sendCommand(self, cmd, cmd_type=None, processed=False, force=False): """ Sends a custom command through the open connection :param cmd: :param cmd_type: :param processed: :param force: :return: """ cmd = cmd.encode('ascii', 'replace') if not processed: cmd = comm.process_gcode_line(cmd) if not cmd: return #if self.isPrinting() and not self.isSdFileSelected(): # self._commandQueue.put((cmd, cmd_type)) if self.isOperational(): wait = None if "g" in cmd.lower(): wait = "3" resp = self._beeCommands.sendCmd(cmd, wait) if resp: # puts the response in the monitor queue self._responseQueue.put(resp) # logs the command reply with errors splits = resp.rstrip().split("\n") for r in splits: if "Error" in r: self._logger.warning(r) return True else: return False def close(self, is_error=False, wait=True, timeout=10.0, *args, **kwargs): """ Closes the connection to the printer if it's active :param is_error: :param wait: unused parameter (kept for interface compatibility) :param timeout: :param args: :param kwargs: :return: """ if self._beeCommands is not None: self._beeCommands.stopStatusMonitor() if self._beeConn is not None: self._beeConn.close() self._changeState(self.STATE_CLOSED) def _changeState(self, newState): if self._state == newState: return oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.on_comm_state_change(newState) def confirmConnection(self): """ Confirms the connection changing the internal state of the printer :return: """ if self._beeConn.isConnected(): if self._beeCommands.isPrinting(): self._changeState(self.STATE_PRINTING) elif self._beeCommands.isShutdown(): self._changeState(self.STATE_SHUTDOWN) elif self._beeCommands.isPaused(): self._changeState(self.STATE_PAUSED) else: self._changeState(self.STATE_OPERATIONAL) else: self._changeState(self.STATE_WAITING_FOR_BTF) def getConnectedPrinterName(self): """ Returns the current connected printer name :return: """ if self._beeConn is not None: return self._beeConn.getConnectedPrinterName() else: return "" def getConnectedPrinterSN(self): """ Returns the current connected printer serial number :return: """ if self._beeConn is not None: return self._beeConn.getConnectedPrinterSN() else: return None def isOperational(self): return self._state == self.STATE_OPERATIONAL \ or self._state == self.STATE_PRINTING \ or self._state == self.STATE_PAUSED \ or self._state == self.STATE_SHUTDOWN \ or self._state == self.STATE_TRANSFERING_FILE \ or self._state == self.STATE_PREPARING_PRINT \ or self._state == self.STATE_HEATING def isClosedOrError(self): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR \ or self._state == self.STATE_CLOSED or self._state == self.STATE_WAITING_FOR_BTF def isBusy(self): return self.isPrinting() or self.isPaused() or self.isPreparingPrint() def isPreparingPrint(self): return self._state == self.STATE_PREPARING_PRINT or self._state == self.STATE_HEATING def isPrinting(self): return self._state == self.STATE_PRINTING def isHeating(self): return self._state == self.STATE_HEATING def isShutdown(self): return self._state == self.STATE_SHUTDOWN def getStateString(self): """ Returns the current printer state :return: """ if self._state == self.STATE_WAITING_FOR_BTF or self._state == self.STATE_CLOSED: return "Disconnected" elif self._state == self.STATE_PREPARING_PRINT: return "Preparing..." elif self._state == self.STATE_HEATING: return "Heating" elif self._state == self.STATE_SHUTDOWN: return "Shutdown" elif self._state == self.STATE_OPERATIONAL: return "Ready" else: return super(BeeCom, self).getStateString() def startPrint(self, pos=None): """ Starts the printing operation :param pos: if the string 'memory' is passed the printer will print the last file in the printer's memory """ if not self.isOperational() or self.isPrinting(): return if self._currentFile is None and pos is None: raise ValueError("No file selected for printing") try: self._changeState(self.STATE_PREPARING_PRINT) if self.isSdFileSelected(): print_resp = self._beeCommands.startSDPrint(self._currentFile.getFilename()) if print_resp: self._sd_status_timer = RepeatedTimer(self._timeout_intervals.get("sdStatus", 1.0), self._poll_sd_status, run_first=True) self._sd_status_timer.start() elif pos == 'from_memory': print_resp = self._beeCommands.repeatLastPrint() else: print_resp = self._beeCommands.printFile(self._currentFile.getFilename()) if print_resp is True: self._heatupWaitStartTime = time.time() self._heatupWaitTimeLost = 0.0 self._pauseWaitStartTime = 0 self._pauseWaitTimeLost = 0.0 self._heating = True self._preparing_print = True self._prepare_print_thread = threading.Thread(target=self._preparePrintThread, name="comm._preparePrint") self._prepare_print_thread.daemon = True self._prepare_print_thread.start() else: self._errorValue = "Error while preparing the printing operation." self._logger.exception(self._errorValue) self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return except: self._errorValue = get_exception_string() self._logger.exception("Error while trying to start printing: " + self.getErrorString()) self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def cancelPrint(self, firmware_error=None): """ Cancels the print operation :type firmware_error: unused parameter, just to keep the interface compatible with octoprint """ if not self.isOperational() or self.isStreaming(): return self._preparing_print = False if self._beeCommands.cancelPrint(): self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): if self._sd_status_timer is not None: try: self._sd_status_timer.cancel() except: pass else: self._logger.exception("Error while canceling the print operation.") eventManager().fire(Events.ERROR, {"error": "Error canceling print"}) return def setPause(self, pause): """ Toggle Pause method :param pause: True to pause or False to unpause :return: """ if self.isStreaming(): return if not self._currentFile: return payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } if (not pause and self.isPaused()) or (not pause and self.isShutdown()): if self._pauseWaitStartTime: self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) self._pauseWaitStartTime = None # resumes printing self._beeCommands.resumePrint() self._heating = True self._resume_print_thread = threading.Thread(target=self._resumePrintThread, name="comm._resumePrint") self._resume_print_thread.daemon = True self._resume_print_thread.start() elif pause and self.isPrinting(): if not self._pauseWaitStartTime: self._pauseWaitStartTime = time.time() # pause print self._beeCommands.pausePrint() self._changeState(self.STATE_PAUSED) eventManager().fire(Events.PRINT_PAUSED, payload) def enterShutdownMode(self): """ Enters the printer shutdown mode :return: """ if self.isStreaming(): return if not self._currentFile: return payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } # enter shutdown mode self._beeCommands.enterShutdown() self._changeState(self.STATE_SHUTDOWN) eventManager().fire(Events.POWER_OFF, payload) def initSdCard(self): """ Initializes the SD Card in the printer :return: """ if not self.isOperational(): return self._beeCommands.initSD() if settings().getBoolean(["feature", "sdAlwaysAvailable"]): self._sdAvailable = True self.refreshSdFiles() self._callback.on_comm_sd_state_change(self._sdAvailable) def refreshSdFiles(self): """ Refreshes the list of available SD card files :return: """ if not self.isOperational() or self.isBusy(): return fList = self._beeCommands.getFileList() ##~~ SD file list if len(fList) > 0 and 'FileNames' in fList: for sdFile in fList['FileNames']: if comm.valid_file_type(sdFile, "machinecode"): if comm.filter_non_ascii(sdFile): self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) else: if not filename.startswith("/"): # file from the root of the sd -- we'll prepend a / filename = "/" + filename self._sdFiles.append((sdFile, 0)) continue def startFileTransfer(self, filename, localFilename, remoteFilename): """ Transfers a file to the printer's SD Card """ if not self.isOperational() or self.isBusy(): self._log("Printer is not operation or busy") return self._currentFile = comm.StreamingGcodeFileInformation(filename, localFilename, remoteFilename) self._currentFile.start() # starts the transfer self._beeCommands.transferSDFile(filename, localFilename) eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename}) self._callback.on_comm_file_transfer_started(remoteFilename, self._currentFile.getFilesize()) # waits for transfer to end while self._beeCommands.getTransferCompletionState() > 0: time.sleep(2) remote = self._currentFile.getRemoteFilename() payload = { "local": self._currentFile.getLocalFilename(), "remote": remote, "time": self.getPrintTime() } self._currentFile = None self._changeState(self.STATE_OPERATIONAL) self._callback.on_comm_file_transfer_done(remote) eventManager().fire(Events.TRANSFER_DONE, payload) self.refreshSdFiles() def startPrintStatusProgressMonitor(self): """ Starts the monitor thread that keeps track of the print progress :return: """ if self._beeCommands is not None: # starts the progress status thread self._beeCommands.startStatusMonitor(self._statusProgressQueueCallback) def selectFile(self, filename, sd): """ Overrides the original selectFile method to allow to select files when printer is busy. For example when reconnecting after connection was lost and the printer is still printing :param filename: :param sd: :return: """ if sd: if not self.isOperational(): # printer is not connected, can't use SD return self._sdFileToSelect = filename self.sendCommand("M23 %s" % filename) else: # Special case treatment for in memory file printing if filename == 'Memory File': self._currentFile = InMemoryFileInformation(filename, offsets_callback=self.getOffsets, current_tool_callback=self.getCurrentTool) self._callback.on_comm_file_selected(filename, 0, False) else: self._currentFile = comm.PrintingGcodeFileInformation(filename, offsets_callback=self.getOffsets, current_tool_callback=self.getCurrentTool) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) def getPrintProgress(self): """ Gets the current print progress :return: """ if self._currentFile is None: return None return self._currentFile.getProgress() def getCurrentFile(self): """ Gets the current PrintFileInformation object :return: """ return self._currentFile def _getResponse(self): """ Auxiliar method to read the command response queue :return: """ if self._beeConn is None: return None try: ret = self._responseQueue.get() except: self._log("Exception raised while reading from command response queue: %s" % (get_exception_string())) self._errorValue = get_exception_string() return None if ret == '': #self._log("Recv: TIMEOUT") return '' try: self._log("Recv: %s" % sanitize_ascii(ret)) except ValueError as e: self._log("WARN: While reading last line: %s" % e) self._log("Recv: %r" % ret) return ret def triggerPrintFinished(self): """ This method runs the post-print job code :return: """ self._sdFilePos = 0 self._callback.on_comm_print_job_done() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime() }) if self._sd_status_timer is not None: try: self._sd_status_timer.cancel() except: pass def _monitor(self): """ Monitor thread of responses from the commands sent to the printer :return: """ feedback_controls, feedback_matcher = comm.convert_feedback_controls(settings().get(["controls"])) feedback_errors = [] pause_triggers = comm.convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"])) #exits if no connection is active if not self._beeConn.isConnected(): return startSeen = False supportWait = settings().getBoolean(["feature", "supportWait"]) while self._monitoring_active: try: line = self._getResponse() if line is None: continue ##~~ debugging output handling if line.startswith("//"): debugging_output = line[2:].strip() if debugging_output.startswith("action:"): action_command = debugging_output[len("action:"):].strip() if action_command == "pause": self._log("Pausing on request of the printer...") self.setPause(True) elif action_command == "resume": self._log("Resuming on request of the printer...") self.setPause(False) elif action_command == "disconnect": self._log("Disconnecting on request of the printer...") self._callback.on_comm_force_disconnect() else: for hook in self._printer_action_hooks: try: self._printer_action_hooks[hook](self, line, action_command) except: self._logger.exception("Error while calling hook {} with action command {}".format(self._printer_action_hooks[hook], action_command)) continue else: continue ##~~ Error handling line = self._handleErrors(line) ##~~ process oks if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")): self._clear_to_send.set() self._long_running_command = False ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') \ or ' B:' in line or line.startswith('B:'): self._processTemperatures(line) self._callback.on_comm_temperature_update(self._temp, self._bedTemp) ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line and not self._sdAvailable: self._sdAvailable = True self.refreshSdFiles() self._callback.on_comm_sd_state_change(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.on_comm_sd_files(self._sdFiles) elif 'SD printing byte' in line and self.isSdPrinting(): # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) self._callback.on_comm_progress() elif 'File opened' in line and not self._ignore_select: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = regex_sdFileOpened.search(line) if self._sdFileToSelect: name = self._sdFileToSelect self._sdFileToSelect = None else: name = match.group(1) self._currentFile = comm.PrintingSdFileInformation(name, int(match.group(2))) elif 'File selected' in line: if self._ignore_select: self._ignore_select = False elif self._currentFile is not None: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.on_comm_file_selected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) elif 'Writing to file' in line: # answer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._changeState(self.STATE_PRINTING) self._clear_to_send.set() line = "ok" elif 'Done saving file' in line: self.refreshSdFiles() elif 'File deleted' in line and line.strip().endswith("ok"): # buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in # current versions self._clear_to_send.set() ##~~ Message handling elif line.strip() != '' \ and line.strip() != 'ok' and not line.startswith("wait") \ and not line.startswith('Resend:') \ and line != 'echo:Unknown command:""\n' \ and self.isOperational(): self._callback.on_comm_message(line) ##~~ Parsing for feedback commands if feedback_controls and feedback_matcher and not "_all" in feedback_errors: try: self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors) except: # something went wrong while feedback matching self._logger.exception("Error while trying to apply feedback control matching, disabling it") feedback_errors.append("_all") ##~~ Parsing for pause triggers if pause_triggers and not self.isStreaming(): if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) self.setPause(not self.isPaused()) ### Connection attempt elif self._state == self.STATE_CONNECTING: if "start" in line and not startSeen: startSeen = True self._sendCommand("M110") self._clear_to_send.set() elif "ok" in line: self._onConnected() elif time.time() > self._timeout: self.close() ### Operational elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: if "ok" in line: # if we still have commands to process, process them if self._resendDelta is not None: self._resendNextCommand() elif self._sendFromQueue(): pass # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): self._handleResendRequest(line) except Exception as ex: self._logger.exception("Something crashed inside the USB connection.") errorMsg = "See octoprint.log for details" self._log(ex.message) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) self._log("Connection closed, closing down monitor") def _statusProgressQueueCallback(self, status_obj): """ Auxiliar callback method to push the status object that comes from the printer into the queue :param status_obj: :return: """ # calls the Printer object to update the progress values self._callback.updateProgress(status_obj) self._callback.on_comm_progress() def _onConnected(self): """ Post connection callback """ # starts the connection monitor thread self._beeConn.startConnectionMonitor() self._temperature_timer = RepeatedTimer(self._timeout_intervals.get("temperature", 4.0), self._poll_temperature, run_first=True) self._temperature_timer.start() if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() payload = dict(port=self._port, baudrate=self._baudrate) eventManager().fire(Events.CONNECTED, payload) def _poll_temperature(self): """ Polls the temperature after the temperature timeout, re-enqueues itself. If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll will be done. """ try: if self.isOperational() and not self.isStreaming() and not self._long_running_command and not self._heating: self.sendCommand("M105", cmd_type="temperature_poll") except Exception as e: self._log("Error polling temperature %s" % str(e)) def getCommandsInterface(self): """ Returns the commands interface for BVC printers :return: """ return self._beeCommands def _connShutdownHook(self): """ Function to be called by the BVC driver to shutdown the connection :return: """ self._callback.on_comm_force_disconnect() def _preparePrintThread(self): """ Thread code that runs while the print job is being prepared :return: """ # waits for heating/file transfer while self._beeCommands.isTransferring(): time.sleep(1) if not self._preparing_print: # the print (transfer) was cancelled return self._changeState(self.STATE_HEATING) while self._beeCommands.isHeating(): time.sleep(1) if not self._preparing_print: # the print (heating) was cancelled return if self._currentFile is not None: # Starts the real printing operation self._currentFile.start() self._changeState(self.STATE_PRINTING) payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_STARTED, payload) # starts the progress status thread self.startPrintStatusProgressMonitor() if self._heatupWaitStartTime is not None: self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) self._heatupWaitStartTime = None self._heating = False self._preparing_print = False else: self._changeState(self.STATE_READY) self._logger.error('Error starting Print operation. No selected file found.') def _resumePrintThread(self): """ Thread code that runs while the print job is being resumed after pause/shutdown :return: """ self._changeState(self.STATE_HEATING) while self._beeCommands.isHeating(): time.sleep(1) if not self._preparing_print: # the print (heating) was cancelled return if self._currentFile is not None: # Starts the real printing operation self._changeState(self.STATE_PRINTING) payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_RESUMED, payload) # starts the progress status thread self.startPrintStatusProgressMonitor() if self._heatupWaitStartTime is not None: self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) self._heatupWaitStartTime = None self._heating = False self._preparing_print = False else: self._changeState(self.STATE_READY) self._logger.error('Error starting Print operation. No selected file found.')
class PiSupportPlugin(octoprint.plugin.EnvironmentDetectionPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin): # noinspection PyMissingConstructor def __init__(self): self._throttle_state = ThrottleState() self._throttle_check = None self._throttle_undervoltage = False self._throttle_overheat = False self._throttle_functional = True #~~ EnvironmentDetectionPlugin def get_additional_environment(self): result = dict(model=get_proc_dt_model()) if is_octopi(): result.update(dict(octopi_version=get_octopi_version())) return result #~~ SimpleApiPlugin def on_api_get(self, request): result = dict(throttle_state=self._throttle_state.as_dict()) result.update(self.get_additional_environment()) return flask.jsonify(**result) #~~ AssetPlugin def get_assets(self): return dict( js=["js/pi_support.js"], clientjs=["clientjs/pi_support.js"], css=["css/pi_support.css"] ) #~~ TemplatePlugin def get_template_configs(self): configs = [dict(type="settings", name=gettext("Pi Support"), template="pi_support_settings.jinja2", custom_bindings=False)] if is_octopi(): configs.append(dict(type="about", name="About OctoPi", template="pi_support_about_octopi.jinja2")) return configs def get_template_vars(self): return self.get_additional_environment() #~~ StartupPlugin def on_startup(self, *args, **kwargs): if self._settings.get_boolean(["vcgencmd_throttle_check_enabled"]): self._check_throttled_state() self._throttle_check = RepeatedTimer(self._check_throttled_state_interval, self._check_throttled_state, condition=self._check_throttled_state_condition) self._throttle_check.start() #~~ SettingsPlugin def get_settings_defaults(self): return dict(vcgencmd_throttle_check_enabled=True, vcgencmd_throttle_check_command=_VCGENCMD_THROTTLE) def get_settings_restricted_paths(self): return dict(admin=[["vcgencmd_throttle_check_enabled"], ["vcgencmd_throttle_check_command"]]) #~~ Helpers def _check_throttled_state_interval(self): if self._throttle_state.current_issue: # check state every 30s if something's currently amiss return 30 else: # check state every 5min if nothing's currently amiss return 300 def _check_throttled_state_condition(self): return self._throttle_functional def get_throttle_state(self, run_now=False): """Exposed as public helper.""" if run_now: self._check_throttled_state() if not self._throttle_functional: return False return self._throttle_state.as_dict() def _check_throttled_state(self): command = self._settings.get(["vcgencmd_throttle_check_command"]) self._logger.debug("Retrieving throttle state via \"{}\"".format(command)) try: state = get_vcgencmd_throttled_state(command) except: self._logger.exception("Got an error while trying to fetch the current throttle state via \"{}\"".format(command)) self._throttle_functional = False return if self._throttle_state == state: # no change return self._throttle_state = state if (not self._throttle_undervoltage and self._throttle_state.undervoltage) \ or (not self._throttle_overheat and self._throttle_state.overheat): message = "This Raspberry Pi is reporting problems that might lead to bad performance or errors caused " \ "by overheating or insufficient power." if self._throttle_state.undervoltage: self._throttle_undervoltage = True message += "\n!!! UNDERVOLTAGE REPORTED !!! Make sure that the power supply and power cable are " \ "capable of supplying enough voltage and current to your Pi." if self._throttle_state.overheat: self._throttle_overheat = True message += "\n!!! FREQUENCY CAPPING DUE TO OVERHEATING REPORTED !!! Improve cooling on the Pi's " \ "CPU and GPU." self._logger.warn(message) self._plugin_manager.send_plugin_message(self._identifier, dict(type="throttle_state", state=self._throttle_state.as_dict())) # noinspection PyUnresolvedReferences self._event_bus.fire(octoprint.events.Events.PLUGIN_PI_SUPPORT_THROTTLE_STATE, self._throttle_state.as_dict())
class ConnectivityChecker(object): """ Regularly checks for online connectivity. Tries to open a connection to the provided ``host`` and ``port`` every ``interval`` seconds and sets the ``online`` status accordingly. """ def __init__(self, interval, host, port, enabled=True, on_change=None): self._interval = interval self._host = host self._port = port self._enabled = enabled self._on_change = on_change self._logger = logging.getLogger(__name__ + ".connectivity_checker") # we initialize the online flag to True if we are not enabled (we don't know any better # but these days it's probably a sane default) self._online = not self._enabled self._check_worker = None self._check_mutex = threading.RLock() self._run() @property def online(self): """Current online status, True if online, False if offline.""" with self._check_mutex: return self._online @property def host(self): """DNS host to query.""" with self._check_mutex: return self._host @host.setter def host(self, value): with self._check_mutex: self._host = value @property def port(self): """DNS port to query.""" with self._check_mutex: return self._port @port.setter def port(self, value): with self._check_mutex: self._port = value @property def interval(self): """Interval between consecutive automatic checks.""" return self._interval @interval.setter def interval(self, value): self._interval = value @property def enabled(self): """Whether the check is enabled or not.""" return self._enabled @enabled.setter def enabled(self, value): with self._check_mutex: old_enabled = self._enabled self._enabled = value if not self._enabled: if self._check_worker is not None: self._check_worker.cancel() old_value = self._online self._online = True if old_value != self._online: self._trigger_change(old_value, self._online) elif self._enabled and not old_enabled: self._run() def check_immediately(self): """Check immediately and return result.""" with self._check_mutex: self._perform_check() return self.online def _run(self): from octoprint.util import RepeatedTimer if not self._enabled: return if self._check_worker is not None: self._check_worker.cancel() self._check_worker = RepeatedTimer(self._interval, self._perform_check, run_first=True) self._check_worker.start() def _perform_check(self): if not self._enabled: return with self._check_mutex: self._logger.debug("Checking against {}:{} if we are online...".format(self._host, self._port)) old_value = self._online self._online = server_reachable(self._host, port=self._port) if old_value != self._online: self._trigger_change(old_value, self._online) def _trigger_change(self, old_value, new_value): self._logger.info("Connectivity changed from {} to {}".format("online" if old_value else "offline", "online" if new_value else "offline")) if callable(self._on_change): self._on_change(old_value, new_value)
class VirtualPrinter(object): command_regex = re.compile("^([GMTF])(\d+)") sleep_regex = re.compile("sleep (\d+)") sleep_after_regex = re.compile("sleep_after ([GMTF]\d+) (\d+)") sleep_after_next_regex = re.compile("sleep_after_next ([GMTF]\d+) (\d+)") custom_action_regex = re.compile("action_custom ([a-zA-Z0-9_]+)(\s+.*)?") prepare_ok_regex = re.compile("prepare_ok (.*)") send_regex = re.compile("send (.*)") set_ambient_regex = re.compile("set_ambient ([-+]?[0-9]*\.?[0-9]+)") start_sd_regex = re.compile("start_sd (.*)") select_sd_regex = re.compile("select_sd (.*)") def __init__(self, seriallog_handler=None, read_timeout=5.0, write_timeout=10.0): import logging self._logger = logging.getLogger("octoprint.plugins.virtual_printer.VirtualPrinter") self._seriallog = logging.getLogger("octoprint.plugin.virtual_printer.VirtualPrinter.serial") self._seriallog.setLevel(logging.CRITICAL) self._seriallog.propagate = False if seriallog_handler is not None: import logging.handlers self._seriallog.addHandler(seriallog_handler) self._seriallog.setLevel(logging.INFO) self._seriallog.info("-"*78) self._read_timeout = read_timeout self._write_timeout = write_timeout self._rx_buffer_size = settings().getInt(["devel", "virtualPrinter", "rxBuffer"]) self.incoming = CharCountingQueue(self._rx_buffer_size, name="RxBuffer") self.outgoing = queue.Queue() self.buffered = queue.Queue(maxsize=settings().getInt(["devel", "virtualPrinter", "commandBuffer"])) if settings().getBoolean(["devel", "virtualPrinter", "simulateReset"]): for item in settings().get(["devel", "virtualPrinter", "resetLines"]): self._send(item + "\n") self._prepared_oks = [] prepared = settings().get(["devel", "virtualPrinter", "preparedOks"]) if prepared and isinstance(prepared, list): for prep in prepared: self._prepared_oks.append(prep) self._prepared_errors = [] self._errors = settings().get(["devel", "virtualPrinter", "errors"], merged=True) self.currentExtruder = 0 self.extruderCount = settings().getInt(["devel", "virtualPrinter", "numExtruders"]) self.pinnedExtruders = settings().get(["devel", "virtualPrinter", "pinnedExtruders"]) if self.pinnedExtruders is None: self.pinnedExtruders = dict() self.sharedNozzle = settings().getBoolean(["devel", "virtualPrinter", "sharedNozzle"]) self.temperatureCount = (1 if self.sharedNozzle else self.extruderCount) self._ambient_temperature = settings().getFloat(["devel", "virtualPrinter", "ambientTemperature"]) self.temp = [self._ambient_temperature] * self.temperatureCount self.targetTemp = [0.0] * self.temperatureCount self.bedTemp = self._ambient_temperature self.bedTargetTemp = 0.0 self.lastTempAt = time.time() self._relative = True self._lastX = 0.0 self._lastY = 0.0 self._lastZ = 0.0 self._lastE = [0.0] * self.extruderCount self._lastF = 200 self._unitModifier = 1 self._feedrate_multiplier = 100 self._flowrate_multiplier = 100 self._virtualSd = settings().getBaseFolder("virtualSd") self._sdCardReady = True self._sdPrinter = None self._sdPrintingSemaphore = threading.Event() self._selectedSdFile = None self._selectedSdFileSize = None self._selectedSdFilePos = None self._writingToSd = False self._writingToSdHandle = None self._newSdFilePos = None self._heatingUp = False self._okBeforeCommandOutput = settings().getBoolean(["devel", "virtualPrinter", "okBeforeCommandOutput"]) self._supportM112 = settings().getBoolean(["devel", "virtualPrinter", "supportM112"]) self._supportF = settings().getBoolean(["devel", "virtualPrinter", "supportF"]) self._sendWait = settings().getBoolean(["devel", "virtualPrinter", "sendWait"]) self._sendBusy = settings().getBoolean(["devel", "virtualPrinter", "sendBusy"]) self._waitInterval = settings().getFloat(["devel", "virtualPrinter", "waitInterval"]) self._busyInterval = settings().getFloat(["devel", "virtualPrinter", "busyInterval"]) self._echoOnM117 = settings().getBoolean(["devel", "virtualPrinter", "echoOnM117"]) self._brokenM29 = settings().getBoolean(["devel", "virtualPrinter", "brokenM29"]) self._brokenResend = settings().getBoolean(["devel", "virtualPrinter", "brokenResend"]) self._m115FormatString = settings().get(["devel", "virtualPrinter", "m115FormatString"]) self._firmwareName = settings().get(["devel", "virtualPrinter", "firmwareName"]) self._okFormatString = settings().get(["devel", "virtualPrinter", "okFormatString"]) self._capabilities = settings().get(["devel", "virtualPrinter", "capabilities"], merged=True) self._temperature_reporter = None self._sdstatus_reporter = None self.currentLine = 0 self.lastN = 0 self._incoming_lock = threading.RLock() self._debug_awol = False self._debug_sleep = None self._sleepAfterNext = dict() self._sleepAfter = dict() self._dont_answer = False self._debug_drop_connection = False self._action_hooks = plugin_manager().get_hooks("octoprint.plugin.virtual_printer.custom_action") self._killed = False self._triggerResendAt100 = True self._triggerResendWithTimeoutAt105 = True self._triggerResendWithMissingLinenoAt110 = True self._triggerResendWithChecksumMismatchAt115 = True readThread = threading.Thread(target=self._processIncoming, name="octoprint.plugins.virtual_printer.wait_thread") readThread.start() bufferThread = threading.Thread(target=self._processBuffer, name="octoprint.plugins.virtual_printer.buffer_thread") bufferThread.start() def __str__(self): return "VIRTUAL(read_timeout={read_timeout},write_timeout={write_timeout},options={options})"\ .format(read_timeout=self._read_timeout, write_timeout=self._write_timeout, options=settings().get(["devel", "virtualPrinter"])) def _reset(self): with self._incoming_lock: self._relative = True self._lastX = 0.0 self._lastY = 0.0 self._lastZ = 0.0 self._lastE = [0.0] * self.extruderCount self._lastF = 200 self._unitModifier = 1 self._feedrate_multiplier = 100 self._flowrate_multiplier = 100 self._sdCardReady = True self._sdPrinting = False if self._sdPrinter: self._sdPrinting = False self._sdPrintingSemaphore.set() self._sdPrinter = None self._selectedSdFile = None self._selectedSdFileSize = None self._selectedSdFilePos = None if self._writingToSdHandle: try: self._writingToSdHandle.close() except: pass self._writingToSd = False self._writingToSdHandle = None self._newSdFilePos = None self._heatingUp = False self.currentLine = 0 self.lastN = 0 self._debug_awol = False self._debug_sleep = None self._sleepAfterNext.clear() self._sleepAfter.clear() self._dont_answer = False self._debug_drop_connection = False self._killed = False self._triggerResendAt100 = True self._triggerResendWithTimeoutAt105 = True self._triggerResendWithMissingLinenoAt110 = True self._triggerResendWithChecksumMismatchAt115 = True if self._temperature_reporter is not None: self._temperature_reporter.cancel() self._temperature_reporter = None if self._sdstatus_reporter is not None: self._sdstatus_reporter.cancel() self._sdstatus_reporter = None self._clearQueue(self.incoming) self._clearQueue(self.outgoing) self._clearQueue(self.buffered) if settings().getBoolean(["devel", "virtualPrinter", "simulateReset"]): for item in settings().get(["devel", "virtualPrinter", "resetLines"]): self._send(item + "\n") @property def timeout(self): return self._read_timeout @timeout.setter def timeout(self, value): self._logger.debug("Setting read timeout to {}s".format(value)) self._read_timeout = value @property def write_timeout(self): return self._write_timeout @write_timeout.setter def write_timeout(self, value): self._logger.debug("Setting write timeout to {}s".format(value)) self._write_timeout = value def _clearQueue(self, q): try: while q.get(block=False): q.task_done() continue except queue.Empty: pass def _processIncoming(self): next_wait_timeout = time.time() + self._waitInterval buf = "" while self.incoming is not None and not self._killed: self._simulateTemps() if self._heatingUp: time.sleep(1) continue try: data = self.incoming.get(timeout=0.01) self.incoming.task_done() except queue.Empty: if self._sendWait and time.time() > next_wait_timeout: self._send("wait") next_wait_timeout = time.time() + self._waitInterval continue buf += data if "\n" in buf: data = buf[:buf.find("\n") + 1] buf = buf[buf.find("\n") + 1:] else: continue next_wait_timeout = time.time() + self._waitInterval if data is None: continue if self._dont_answer: self._dont_answer = False continue data = data.strip() # strip checksum if "*" in data: checksum = int(data[data.rfind("*") + 1:]) data = data[:data.rfind("*")] if not checksum == self._calculate_checksum(data): self._triggerResend(expected=self.currentLine + 1) continue self.currentLine += 1 elif settings().getBoolean(["devel", "virtualPrinter", "forceChecksum"]): self._send(self._error("checksum_missing")) continue # track N = N + 1 if data.startswith("N") and "M110" in data: linenumber = int(re.search("N([0-9]+)", data).group(1)) self.lastN = linenumber self.currentLine = linenumber self._triggerResendAt100 = True self._triggerResendWithTimeoutAt105 = True self._sendOk() continue elif data.startswith("N"): linenumber = int(re.search("N([0-9]+)", data).group(1)) expected = self.lastN + 1 if linenumber != expected: self._triggerResend(actual=linenumber) continue elif linenumber == 100 and self._triggerResendAt100: # simulate a resend at line 100 self._triggerResendAt100 = False self._triggerResend(expected=100) continue elif linenumber == 105 and self._triggerResendWithTimeoutAt105 and not self._writingToSd: # simulate a resend with timeout at line 105 self._triggerResendWithTimeoutAt105 = False self._triggerResend(expected=105) self._dont_answer = True self.lastN = linenumber continue elif linenumber == 110 and self._triggerResendWithMissingLinenoAt110 and not self._writingToSd: self._triggerResendWithMissingLinenoAt110 = False self._send(self._error("lineno_missing", self.lastN)) continue elif linenumber == 115 and self._triggerResendWithChecksumMismatchAt115 and not self._writingToSd: self._triggerResendWithChecksumMismatchAt115 = False self._triggerResend(checksum=True) continue elif len(self._prepared_errors): prepared = self._prepared_errors.pop(0) if callable(prepared): prepared(linenumber, self.lastN, data) continue elif isinstance(prepared, basestring): self._send(prepared) continue else: self.lastN = linenumber data = data.split(None, 1)[1].strip() data += "\n" if data.startswith("!!DEBUG:") or data.strip() == "!!DEBUG": debug_command = "" if data.startswith("!!DEBUG:"): debug_command = data[len("!!DEBUG:"):].strip() self._debugTrigger(debug_command) continue # shortcut for writing to SD if self._writingToSd and self._writingToSdHandle is not None and not "M29" in data: self._writingToSdHandle.write(data) self._sendOk() continue if data.strip() == "version": from octoprint import __version__ self._send("OctoPrint VirtualPrinter v" + __version__) continue # if we are sending oks before command output, send it now if len(data.strip()) > 0 and self._okBeforeCommandOutput: self._sendOk() # actual command handling command_match = VirtualPrinter.command_regex.match(data) if command_match is not None: command = command_match.group(0) letter = command_match.group(1) try: # if we have a method _gcode_G, _gcode_M or _gcode_T, execute that first letter_handler = "_gcode_{}".format(letter) if hasattr(self, letter_handler): code = command_match.group(2) handled = getattr(self, letter_handler)(code, data) if handled: continue # then look for a method _gcode_<command> and execute that if it exists command_handler = "_gcode_{}".format(command) if hasattr(self, command_handler): handled = getattr(self, command_handler)(data) if handled: continue finally: # make sure that the debug sleepAfter and sleepAfterNext stuff works even # if we continued above if len(self._sleepAfter) or len(self._sleepAfterNext): interval = None if command in self._sleepAfter: interval = self._sleepAfter[command] elif command in self._sleepAfterNext: interval = self._sleepAfterNext[command] del self._sleepAfterNext[command] if interval is not None: self._send("// sleeping for {interval} seconds".format(interval=interval)) time.sleep(interval) # if we are sending oks after command output, send it now if len(data.strip()) > 0 and not self._okBeforeCommandOutput: self._sendOk() self._logger.info("Closing down read loop") ##~~ command implementations def _gcode_T(self, code, data): t = int(code) if 0 <= t < self.extruderCount: self.currentExtruder = t self._send("Active Extruder: %d" % self.currentExtruder) else: self._send("echo:T{} Invalid extruder ".format(t)) def _gcode_F(self, code, data): if self._supportF: self._send("echo:changed F value") return False else: self._send(self._error("command_unknown", "F")) return True def _gcode_M104(self, data): self._parseHotendCommand(data) def _gcode_M109(self, data): self._parseHotendCommand(data, wait=True, support_r=True) def _gcode_M140(self, data): self._parseBedCommand(data) def _gcode_M190(self, data): self._parseBedCommand(data, wait=True, support_r=True) def _gcode_M105(self, data): self._processTemperatureQuery() return True def _gcode_M20(self, data): if self._sdCardReady: self._listSd() def _gcode_M21(self, data): self._sdCardReady = True self._send("SD card ok") def _gcode_M22(self, data): self._sdCardReady = False def _gcode_M23(self, data): if self._sdCardReady: filename = data.split(None, 1)[1].strip() self._selectSdFile(filename) def _gcode_M24(self, data): if self._sdCardReady: self._startSdPrint() def _gcode_M25(self, data): if self._sdCardReady: self._pauseSdPrint() def _gcode_M26(self, data): if self._sdCardReady: pos = int(re.search("S([0-9]+)", data).group(1)) self._setSdPos(pos) def _gcode_M27(self, data): def report(): if self._sdCardReady: self._reportSdStatus() match = re.search("S([0-9]+)", data) if match: interval = int(match.group(1)) if self._sdstatus_reporter is not None: self._sdstatus_reporter.cancel() if interval > 0: self._sdstatus_reporter = RepeatedTimer(interval, report) self._sdstatus_reporter.start() else: self._sdstatus_reporter = None report() def _gcode_M28(self, data): if self._sdCardReady: filename = data.split(None, 1)[1].strip() self._writeSdFile(filename) def _gcode_M29(self, data): if self._sdCardReady: self._finishSdFile() def _gcode_M30(self, data): if self._sdCardReady: filename = data.split(None, 1)[1].strip() self._deleteSdFile(filename) def _gcode_M113(self, data): interval = int(re.search("S([0-9]+)", data).group(1)) if 0 <= interval <= 60: self._busyInterval = interval def _gcode_M114(self, data): m114FormatString = settings().get(["devel", "virtualPrinter", "m114FormatString"]) e = dict((index, value) for index, value in enumerate(self._lastE)) e["current"] = self._lastE[self.currentExtruder] e["all"] = " ".join(["E{}:{}".format(num, self._lastE[self.currentExtruder]) for num in range(self.extruderCount)]) output = m114FormatString.format(x=self._lastX, y=self._lastY, z=self._lastZ, e=e, f=self._lastF, a=int(self._lastX*100), b=int(self._lastY*100), c=int(self._lastZ*100)) if not self._okBeforeCommandOutput: ok = self._ok() if ok: output = "{} {}".format(self._ok(), output) self._send(output) return True def _gcode_M115(self, data): output = self._m115FormatString.format(firmware_name=self._firmwareName) self._send(output) if settings().getBoolean(["devel", "virtualPrinter", "m115ReportCapabilities"]): for cap, enabled in self._capabilities.items(): self._send("Cap:{}:{}".format(cap.upper(), "1" if enabled else "0")) def _gcode_M117(self, data): # we'll just use this to echo a message, to allow playing around with pause triggers if self._echoOnM117: self._send("echo:%s" % re.search("M117\s+(.*)", data).group(1)) def _gcode_M155(self, data): interval = int(re.search("S([0-9]+)", data).group(1)) if self._temperature_reporter is not None: self._temperature_reporter.cancel() if interval > 0: self._temperature_reporter = RepeatedTimer(interval, lambda: self._send(self._generateTemperatureOutput())) self._temperature_reporter.start() else: self._temperature_reporter = None def _gcode_M220(self, data): self._feedrate_multiplier = float(re.search('S([0-9]+)', data).group(1)) def _gcode_M221(self, data): self._flowrate_multiplier = float(re.search('S([0-9]+)', data).group(1)) def _gcode_M400(self, data): self.buffered.join() def _gcode_M999(self, data): # mirror Marlin behaviour self._send("Resend: 1") def _gcode_G20(self, data): self._unitModifier = 1.0 / 2.54 if self._lastX is not None: self._lastX *= 2.54 if self._lastY is not None: self._lastY *= 2.54 if self._lastZ is not None: self._lastZ *= 2.54 if self._lastE is not None: self._lastE = [e * 2.54 if e is not None else None for e in self._lastE] def _gcode_G21(self, data): self._unitModifier = 1.0 if self._lastX is not None: self._lastX /= 2.54 if self._lastY is not None: self._lastY /= 2.54 if self._lastZ is not None: self._lastZ /= 2.54 if self._lastE is not None: self._lastE = [e / 2.54 if e is not None else None for e in self._lastE] def _gcode_G90(self, data): self._relative = False def _gcode_G91(self, data): self._relative = True def _gcode_G92(self, data): self._setPosition(data) def _gcode_G28(self, data): self._performMove(data) def _gcode_G0(self, data): # simulate reprap buffered commands via a Queue with maxsize which internally simulates the moves self.buffered.put(data) _gcode_G1 = _gcode_G0 _gcode_G2 = _gcode_G0 _gcode_G3 = _gcode_G0 def _gcode_G4(self, data): matchS = re.search('S([0-9]+)', data) matchP = re.search('P([0-9]+)', data) _timeout = 0 if matchP: _timeout = float(matchP.group(1)) / 1000.0 elif matchS: _timeout = float(matchS.group(1)) if self._sendBusy and self._busyInterval > 0: until = time.time() + _timeout while time.time() < until: time.sleep(self._busyInterval) self._send("busy:processing") else: time.sleep(_timeout) ##~~ further helpers def _calculate_checksum(self, line): checksum = 0 for c in line: checksum ^= ord(c) return checksum def _kill(self): if not self._supportM112: return self._killed = True self._send("echo:EMERGENCY SHUTDOWN DETECTED. KILLED.") def _triggerResend(self, expected=None, actual=None, checksum=None): with self._incoming_lock: if expected is None: expected = self.lastN + 1 else: self.lastN = expected - 1 if actual is None: if checksum: self._send(self._error("checksum_mismatch")) else: self._send(self._error("checksum_missing")) else: self._send(self._error("lineno_mismatch", expected, actual)) def request_resend(): self._send("Resend:%d" % expected) if not self._brokenResend: self._sendOk() request_resend() def _debugTrigger(self, data): if data == "" or data == "help" or data == "?": usage = """ OctoPrint Virtual Printer debug commands help ? | This help. # Action Triggers action_pause | Sends a "// action:pause" action trigger to the host. action_resume | Sends a "// action:resume" action trigger to the host. action_disconnect | Sends a "// action:disconnect" action trigger to the | host. action_custom <action>[ <parameters>] | Sends a custom "// action:<action> <parameters>" | action trigger to the host. # Communication Errors dont_answer | Will not acknowledge the next command. go_awol | Will completely stop replying trigger_resend_lineno | Triggers a resend error with a line number mismatch trigger_resend_checksum | Triggers a resend error with a checksum mismatch trigger_missing_checksum | Triggers a resend error with a missing checksum trigger_missing_lineno | Triggers a "no line number with checksum" error w/o resend request drop_connection | Drops the serial connection prepare_ok <broken ok> | Will cause <broken ok> to be enqueued for use, | will be used instead of actual "ok" # Reply Timing / Sleeping sleep <int:seconds> | Sleep <seconds> s sleep_after <str:command> <int:seconds> | Sleeps <seconds> s after each execution of <command> sleep_after_next <str:command> <int:seconds> | Sleeps <seconds> s after execution of next <command> # SD printing start_sd <str:file> | Select and start printing file <file> from SD select_sd <str:file> | Select file <file> from SD, don't start printing it yet. Use | start_sd to start the print cancel_sd | Cancels an ongoing SD print # Misc send <str:message> | Sends back <message> reset | Simulates a reset. Internal state will be lost. """ for line in usage.split("\n"): self._send("echo: {}".format(line.strip())) elif data == "action_pause": self._send("// action:pause") elif data == "action_resume": self._send("// action:resume") elif data == "action_disconnect": self._send("// action:disconnect") elif data == "dont_answer": self._dont_answer = True elif data == "trigger_resend_lineno": self._prepared_errors.append(lambda cur, last, line: self._triggerResend(expected=last, actual=last+1)) elif data == "trigger_resend_checksum": self._prepared_errors.append(lambda cur, last, line: self._triggerResend(expected=last, checksum=True)) elif data == "trigger_missing_checksum": self._prepared_errors.append(lambda cur, last, line: self._triggerResend(expected=last, checksum=False)) elif data == "trigger_missing_lineno": self._prepared_errors.append(lambda cur, last, line: self._send(self._error("lineno_missing", last))) elif data == "drop_connection": self._debug_drop_connection = True elif data == "reset": self._reset() elif data == "mintemp_error": self._send(self._error("mintemp")) elif data == "maxtemp_error": self._send(self._error("maxtemp")) elif data == "go_awol": self._send("// Going AWOL") self._debug_awol = True elif data == "cancel_sd": if self._sdPrinting and self._sdPrinter: self._pauseSdPrint() self._sdPrinting = False self._sdPrintingSemaphore.set() self._sdPrinter.join() self._finishSdPrint() else: try: sleep_match = VirtualPrinter.sleep_regex.match(data) sleep_after_match = VirtualPrinter.sleep_after_regex.match(data) sleep_after_next_match = VirtualPrinter.sleep_after_next_regex.match(data) custom_action_match = VirtualPrinter.custom_action_regex.match(data) prepare_ok_match = VirtualPrinter.prepare_ok_regex.match(data) send_match = VirtualPrinter.send_regex.match(data) set_ambient_match = VirtualPrinter.set_ambient_regex.match(data) start_sd_match = VirtualPrinter.start_sd_regex.match(data) select_sd_match = VirtualPrinter.select_sd_regex.match(data) if sleep_match is not None: interval = int(sleep_match.group(1)) self._send("// sleeping for {interval} seconds".format(interval=interval)) self._debug_sleep = interval elif sleep_after_match is not None: command = sleep_after_match.group(1) interval = int(sleep_after_match.group(2)) self._sleepAfter[command] = interval self._send("// going to sleep {interval} seconds after each {command}".format(**locals())) elif sleep_after_next_match is not None: command = sleep_after_next_match.group(1) interval = int(sleep_after_next_match.group(2)) self._sleepAfterNext[command] = interval self._send("// going to sleep {interval} seconds after next {command}".format(**locals())) elif custom_action_match is not None: action = custom_action_match.group(1) params = custom_action_match.group(2) params = params.strip() if params is not None else "" self._send("// action:{action} {params}".format(**locals()).strip()) elif prepare_ok_match is not None: ok = prepare_ok_match.group(1) self._prepared_oks.append(ok) elif send_match is not None: self._send(send_match.group(1)) elif set_ambient_match is not None: self._ambient_temperature = float(set_ambient_match.group(1)) self._send("// set ambient temperature to {}".format(self._ambient_temperature)) elif start_sd_match is not None: self._selectSdFile(start_sd_match.group(1), check_already_open=True) self._startSdPrint() elif select_sd_match is not None: self._selectSdFile(select_sd_match.group(1)) except: pass def _listSd(self): self._send("Begin file list") if settings().getBoolean(["devel", "virtualPrinter", "extendedSdFileList"]): items = map( lambda x: "%s %d" % (x.upper(), os.stat(os.path.join(self._virtualSd, x)).st_size), os.listdir(self._virtualSd) ) else: items = map( lambda x: x.upper(), os.listdir(self._virtualSd) ) for item in items: self._send(item) self._send("End file list") def _selectSdFile(self, filename, check_already_open=False): if filename.startswith("/"): filename = filename[1:] file = os.path.join(self._virtualSd, filename.lower()) if self._selectedSdFile == file and check_already_open: return if not os.path.exists(file) or not os.path.isfile(file): self._send("open failed, File: %s." % filename) else: self._selectedSdFile = file self._selectedSdFileSize = os.stat(file).st_size if settings().getBoolean(["devel", "virtualPrinter", "includeFilenameInOpened"]): self._send("File opened: %s Size: %d" % (filename, self._selectedSdFileSize)) else: self._send("File opened") self._send("File selected") def _startSdPrint(self): if self._selectedSdFile is not None: if self._sdPrinter is None: self._sdPrinting = True self._sdPrinter = threading.Thread(target=self._sdPrintingWorker) self._sdPrinter.start() self._sdPrintingSemaphore.set() def _pauseSdPrint(self): self._sdPrintingSemaphore.clear() def _setSdPos(self, pos): self._newSdFilePos = pos def _reportSdStatus(self): if self._sdPrinter is not None and self._sdPrintingSemaphore.is_set: self._send("SD printing byte %d/%d" % (self._selectedSdFilePos, self._selectedSdFileSize)) else: self._send("Not SD printing") def _generateTemperatureOutput(self): includeTarget = not settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]) # send simulated temperature data if self.temperatureCount > 1: allTemps = [] for i in range(len(self.temp)): allTemps.append((i, self.temp[i], self.targetTemp[i])) allTempsString = " ".join(map(lambda x: "T%d:%.2f /%.2f" % x if includeTarget else "T%d:%.2f" % (x[0], x[1]), allTemps)) if settings().getBoolean(["devel", "virtualPrinter", "smoothieTemperatureReporting"]): allTempsString = allTempsString.replace("T0:", "T:") if settings().getBoolean(["devel", "virtualPrinter", "hasBed"]): if includeTarget: allTempsString = "B:%.2f /%.2f %s" % (self.bedTemp, self.bedTargetTemp, allTempsString) else: allTempsString = "B:%.2f %s" % (self.bedTemp, allTempsString) if settings().getBoolean(["devel", "virtualPrinter", "includeCurrentToolInTemps"]): if includeTarget: output = "T:%.2f /%.2f %s" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder], allTempsString) else: output = "T:%.2f %s" % (self.temp[self.currentExtruder], allTempsString) else: output = allTempsString else: if includeTarget: output = "T:%.2f /%.2f B:%.2f /%.2f" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp) else: output = "T:%.2f B:%.2f" % (self.temp[0], self.bedTemp) output += " @:64\n" return output def _processTemperatureQuery(self): includeOk = not self._okBeforeCommandOutput output = self._generateTemperatureOutput() if includeOk: ok = self._ok() if ok: output = "{} {}".format(ok, output) self._send(output) def _parseHotendCommand(self, line, wait=False, support_r=False): only_wait_if_higher = True tool = 0 toolMatch = re.search('T([0-9]+)', line) if toolMatch: try: tool = int(toolMatch.group(1)) except: pass if tool >= self.temperatureCount: return try: self.targetTemp[tool] = float(re.search('S([0-9]+)', line).group(1)) except: if support_r: try: self.targetTemp[tool] = float(re.search('R([0-9]+)', line).group(1)) only_wait_if_higher = False except: pass if wait: self._waitForHeatup("tool%d" % tool, only_wait_if_higher) if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]): self._send("TargetExtr%d:%d" % (tool, self.targetTemp[tool])) def _parseBedCommand(self, line, wait=False, support_r=False): only_wait_if_higher = True try: self.bedTargetTemp = float(re.search('S([0-9]+)', line).group(1)) except: if support_r: try: self.bedTargetTemp = float(re.search('R([0-9]+)', line).group(1)) only_wait_if_higher = False except: pass if wait: self._waitForHeatup("bed", only_wait_if_higher) if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]): self._send("TargetBed:%d" % self.bedTargetTemp) def _performMove(self, line): matchX = re.search("X([0-9.]+)", line) matchY = re.search("Y([0-9.]+)", line) matchZ = re.search("Z([0-9.]+)", line) matchE = re.search("E([0-9.]+)", line) matchF = re.search("F([0-9.]+)", line) duration = 0.0 if matchF is not None: try: self._lastF = float(matchF.group(1)) except: pass speedXYZ = self._lastF * (self._feedrate_multiplier / 100.0) speedE = self._lastF * (self._flowrate_multiplier / 100.0) if matchX is not None: try: x = float(matchX.group(1)) if self._relative or self._lastX is None: duration = max(duration, x * self._unitModifier / speedXYZ * 60.0) else: duration = max(duration, (x - self._lastX) * self._unitModifier / speedXYZ * 60.0) if self._relative and self._lastX is not None: self._lastX += x else: self._lastX = x except: pass if matchY is not None: try: y = float(matchY.group(1)) if self._relative or self._lastY is None: duration = max(duration, y * self._unitModifier / speedXYZ * 60.0) else: duration = max(duration, (y - self._lastY) * self._unitModifier / speedXYZ * 60.0) if self._relative and self._lastY is not None: self._lastY += y else: self._lastY = y except: pass if matchZ is not None: try: z = float(matchZ.group(1)) if self._relative or self._lastZ is None: duration = max(duration, z * self._unitModifier / speedXYZ * 60.0) else: duration = max(duration, (z - self._lastZ) * self._unitModifier / speedXYZ * 60.0) if self._relative and self._lastZ is not None: self._lastZ += z else: self._lastZ = z except: pass if matchE is not None: try: e = float(matchE.group(1)) lastE = self._lastE[self.currentExtruder] if self._relative or lastE is None: duration = max(duration, e * self._unitModifier / speedE * 60.0) else: duration = max(duration, (e - lastE) * self._unitModifier / speedE * 60.0) if self._relative and lastE is not None: self._lastE[self.currentExtruder] += e else: self._lastE[self.currentExtruder] = e except: pass if duration: duration *= 0.1 if duration > self._read_timeout: slept = 0 while duration - slept > self._read_timeout and not self._killed: time.sleep(self._read_timeout) slept += self._read_timeout else: time.sleep(duration) def _setPosition(self, line): matchX = re.search("X([0-9.]+)", line) matchY = re.search("Y([0-9.]+)", line) matchZ = re.search("Z([0-9.]+)", line) matchE = re.search("E([0-9.]+)", line) if matchX is None and matchY is None and matchZ is None and matchE is None: self._lastX = self._lastY = self._lastZ = self._lastE[self.currentExtruder] = 0 else: if matchX is not None: try: self._lastX = float(matchX.group(1)) except: pass if matchY is not None: try: self._lastY = float(matchY.group(1)) except: pass if matchZ is not None: try: self._lastZ = float(matchZ.group(1)) except: pass if matchE is not None: try: self._lastE[self.currentExtruder] = float(matchE.group(1)) except: pass def _writeSdFile(self, filename): if filename.startswith("/"): filename = filename[1:] file = os.path.join(self._virtualSd, filename).lower() if os.path.exists(file): if os.path.isfile(file): os.remove(file) else: self._send("error writing to file") handle = None try: handle = open(file, "w") except: self._output("error writing to file") if handle is not None: try: handle.close() except: pass self._writingToSdHandle = handle self._writingToSd = True self._selectedSdFile = file self._send("Writing to file: %s" % filename) def _finishSdFile(self): try: self._writingToSdHandle.close() except: pass finally: self._writingToSdHandle = None self._writingToSd = False self._selectedSdFile = None self._output("Done saving file") def _sdPrintingWorker(self): self._selectedSdFilePos = 0 try: with open(self._selectedSdFile, "r") as f: for line in iter(f.readline, ""): if self._killed or not self._sdPrinting: break # reset position if requested by client if self._newSdFilePos is not None: f.seek(self._newSdFilePos) self._newSdFilePos = None # read current file position self._selectedSdFilePos = f.tell() # if we are paused, wait for unpausing self._sdPrintingSemaphore.wait() if self._killed or not self._sdPrinting: break # set target temps if 'M104' in line or 'M109' in line: self._parseHotendCommand(line, wait='M109' in line) elif 'M140' in line or 'M190' in line: self._parseBedCommand(line, wait='M190' in line) elif line.startswith("G0") or line.startswith("G1") or line.startswith("G2") or line.startswith("G3"): # simulate reprap buffered commands via a Queue with maxsize which internally simulates the moves self.buffered.put(line) except AttributeError: if self.outgoing is not None: raise self._finishSdPrint() def _finishSdPrint(self): if not self._killed: self._sdPrintingSemaphore.clear() self._output("Done printing file") self._selectedSdFilePos = 0 self._sdPrinting = False self._sdPrinter = None def _waitForHeatup(self, heater, only_wait_if_higher): delta = 1 delay = 1 last_busy = time.time() self._heatingUp = True try: if heater.startswith("tool"): toolNum = int(heater[len("tool"):]) test = lambda: self.temp[toolNum] < self.targetTemp[toolNum] - delta or (not only_wait_if_higher and self.temp[toolNum] > self.targetTemp[toolNum] + delta) output = lambda: "T:%0.2f" % self.temp[toolNum] elif heater == "bed": test = lambda: self.bedTemp < self.bedTargetTemp - delta or (not only_wait_if_higher and self.bedTemp > self.bedTargetTemp + delta) output = lambda: "B:%0.2f" % self.bedTemp else: return while not self._killed and self._heatingUp and test(): self._simulateTemps(delta=delta) self._output(output()) if self._sendBusy and time.time() - last_busy >= self._busyInterval: self._output("echo:busy: processing") last_busy = time.time() time.sleep(delay) except AttributeError: if self.outgoing is not None: raise finally: self._heatingUp = False def _deleteSdFile(self, filename): if filename.startswith("/"): filename = filename[1:] f = os.path.join(self._virtualSd, filename) if os.path.exists(f) and os.path.isfile(f): os.remove(f) def _simulateTemps(self, delta=0.5): timeDiff = self.lastTempAt - time.time() self.lastTempAt = time.time() def simulate(actual, target, ambient): if target > 0 and abs(actual - target) > delta: goal = target factor = 10 elif not target and abs(actual - ambient) > delta: goal = ambient factor = 2 else: return actual old = actual actual += math.copysign(timeDiff * factor, goal - actual) if math.copysign(1, goal - old) != math.copysign(1, goal - actual): actual = goal return actual for i in range(len(self.temp)): if i in self.pinnedExtruders: self.temp[i] = self.pinnedExtruders[i] continue self.temp[i] = simulate(self.temp[i], self.targetTemp[i], self._ambient_temperature) self.bedTemp = simulate(self.bedTemp, self.bedTargetTemp, self._ambient_temperature) def _processBuffer(self): while self.buffered is not None: try: line = self.buffered.get(timeout=0.5) except queue.Empty: continue if line is None: continue self._performMove(line) self.buffered.task_done() self._logger.info("Closing down buffer loop") def _output(self, line): try: self.outgoing.put(line) except: if self.outgoing is None: pass def write(self, data): if self._debug_awol: return len(data) if self._debug_drop_connection: self._logger.info("Debug drop of connection requested, raising SerialTimeoutException") raise SerialTimeoutException() with self._incoming_lock: if self.incoming is None or self.outgoing is None: return 0 if "M112" in data and self._supportM112: self._seriallog.info("<<< {}".format(data.strip())) self._kill() return len(data) try: written = self.incoming.put(data, timeout=self._write_timeout, partial=True) self._seriallog.info("<<< {}".format(data.strip())) return written except queue.Full: self._logger.info("Incoming queue is full, raising SerialTimeoutException") raise SerialTimeoutException() def readline(self): if self._debug_awol: time.sleep(self._read_timeout) return "" if self._debug_drop_connection: raise SerialTimeoutException() if self._debug_sleep > 0: # if we are supposed to sleep, we sleep not longer than the read timeout # (and then on the next call sleep again if there's time to sleep left) sleep_for = min(self._debug_sleep, self._read_timeout) self._debug_sleep -= sleep_for time.sleep(sleep_for) if self._debug_sleep > 0: # we slept the full read timeout, return an empty line return "" # otherwise our left over timeout is the read timeout minus what we already # slept for timeout = self._read_timeout - sleep_for else: # use the full read timeout as timeout timeout = self._read_timeout try: # fetch a line from the queue, wait no longer than timeout line = self.outgoing.get(timeout=timeout) self._seriallog.info(">>> {}".format(line.strip())) self.outgoing.task_done() return line except queue.Empty: # queue empty? return empty line return "" def close(self): self._killed = True self.incoming = None self.outgoing = None self.buffered = None def _sendOk(self): if self.outgoing is None: return ok = self._ok() if ok: self._send(ok) def _sendWaitAfterTimeout(self, timeout=5): time.sleep(timeout) if self.outgoing is not None: self._send("wait") def _send(self, line): if self.outgoing is not None: self.outgoing.put(line) def _ok(self): ok = self._okFormatString if self._prepared_oks: ok = self._prepared_oks.pop(0) if ok is None: return ok return ok.format(ok, lastN=self.lastN, buffer=self.buffered.maxsize - self.buffered.qsize()) def _error(self, error, *args, **kwargs): return "Error: {}".format(self._errors.get(error).format(*args, **kwargs))
class TasmotaMQTTPlugin( octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.WizardPlugin): def __init__(self): self._logger = logging.getLogger("octoprint.plugins.tasmota_mqtt") self._tasmota_mqtt_logger = logging.getLogger( "octoprint.plugins.tasmota_mqtt.debug") self.abortTimeout = 0 self._timeout_value = None self._abort_timer = None self._countdown_active = False self._waitForHeaters = False self._waitForTimelapse = False self._timelapse_active = False self._skipIdleTimer = False self.powerOffWhenIdle = False self._idleTimer = None self._autostart_file = None self.mqtt_publish = None self.mqtt_subscribe = None self.idleTimeout = None self.idleIgnoreCommands = None self._idleIgnoreCommandsArray = None self.idleTimeoutWaitTemp = None ##~~ SettingsPlugin mixin def get_settings_defaults(self): return dict(arrRelays=[], full_topic_pattern='%topic%/%prefix%/', abortTimeout=30, powerOffWhenIdle=False, idleTimeout=30, idleIgnoreCommands='M105', idleTimeoutWaitTemp=50, debug_logging=False) def get_settings_version(self): return 5 def on_settings_migrate(self, target, current=None): if current is None or current < 3: self._settings.set(['arrRelays'], self.get_settings_defaults()["arrRelays"]) if current == 2: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["automaticShutdownEnabled"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) if current <= 3: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["errorEvent"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) if current <= 4: # Add new fields arrRelays_new = [] for relay in self._settings.get(['arrRelays']): relay["event_on_upload"] = False relay["event_on_startup"] = False arrRelays_new.append(relay) self._settings.set(["arrRelays"], arrRelays_new) def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) old_powerOffWhenIdle = self._settings.get_boolean(["powerOffWhenIdle"]) old_idleTimeout = self._settings.get_int(["idleTimeout"]) old_idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) old_idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) if self.powerOffWhenIdle != old_powerOffWhenIdle: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self.powerOffWhenIdle: self._tasmota_mqtt_logger.debug( "Settings saved, Automatic Power Off Enabled, starting idle timer..." ) self._reset_idle_timer() new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._tasmota_mqtt_logger.setLevel(logging.DEBUG) else: self._tasmota_mqtt_logger.setLevel(logging.INFO) ##~~ StartupPlugin mixin def on_startup(self, host, port): # setup customized logger from octoprint.logging.handlers import CleaningTimedRotatingFileHandler tasmota_mqtt_logging_hnadler = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(postfix="debug"), when="D", backupCount=3) tasmota_mqtt_logging_hnadler.setFormatter( logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) tasmota_mqtt_logging_hnadler.setLevel(logging.DEBUG) self._tasmota_mqtt_logger.addHandler(tasmota_mqtt_logging_hnadler) self._tasmota_mqtt_logger.setLevel( logging.DEBUG if self._settings.get_boolean(["debug_logging"] ) else logging.INFO) self._tasmota_mqtt_logger.propagate = False def on_after_startup(self): helpers = self._plugin_manager.get_helpers("mqtt", "mqtt_publish", "mqtt_subscribe", "mqtt_unsubscribe") if helpers: if "mqtt_subscribe" in helpers: self.mqtt_subscribe = helpers["mqtt_subscribe"] for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( self.generate_mqtt_full_topic(relay, "stat")) self.mqtt_subscribe(self.generate_mqtt_full_topic( relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top=relay["topic"], relayN=relay["relayN"])) if "mqtt_publish" in helpers: self.mqtt_publish = helpers["mqtt_publish"] self.mqtt_publish("octoprint/plugin/tasmota", "OctoPrint-TasmotaMQTT publishing.") if any( map(lambda r: r["event_on_startup"] == True, self._settings.get(["arrRelays"]))): for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( "powering on {} due to startup.".format( relay["topic"])) self.turn_on(relay) if "mqtt_unsubscribe" in helpers: self.mqtt_unsubscribe = helpers["mqtt_unsubscribe"] else: self._plugin_manager.send_plugin_message(self._identifier, dict(noMQTT=True)) self.abortTimeout = self._settings.get_int(["abortTimeout"]) self._tasmota_mqtt_logger.debug("abortTimeout: %s" % self.abortTimeout) self.powerOffWhenIdle = self._settings.get_boolean( ["powerOffWhenIdle"]) self._tasmota_mqtt_logger.debug("powerOffWhenIdle: %s" % self.powerOffWhenIdle) self.idleTimeout = self._settings.get_int(["idleTimeout"]) self._tasmota_mqtt_logger.debug("idleTimeout: %s" % self.idleTimeout) self.idleIgnoreCommands = self._settings.get(["idleIgnoreCommands"]) self._idleIgnoreCommandsArray = self.idleIgnoreCommands.split(',') self._tasmota_mqtt_logger.debug("idleIgnoreCommands: %s" % self.idleIgnoreCommands) self.idleTimeoutWaitTemp = self._settings.get_int( ["idleTimeoutWaitTemp"]) self._tasmota_mqtt_logger.debug("idleTimeoutWaitTemp: %s" % self.idleTimeoutWaitTemp) if self.powerOffWhenIdle: self._tasmota_mqtt_logger.debug( "Starting idle timer due to startup") self._reset_idle_timer() def _on_mqtt_subscription(self, topic, message, retained=None, qos=None, *args, **kwargs): self._tasmota_mqtt_logger.debug( "Received message for {topic}: {message}".format(**locals())) self.mqtt_publish("octoprint/plugin/tasmota", "echo: " + message.decode("utf-8")) newrelays = [] bolRelayStateChanged = False bolForceIdleTimer = False for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{top}".format( **kwargs) and relay["relayN"] == "{relayN}".format( **kwargs ) and relay["currentstate"] != message.decode("utf-8"): bolRelayStateChanged = True relay["currentstate"] = message.decode("utf-8") if relay[ "automaticShutdownEnabled"] == True and self._settings.get_boolean( ["powerOffWhenIdle" ]) and relay["currentstate"] == "ON": self._tasmota_mqtt_logger.debug( "Forcing reset of idle timer because {} was just turned on." .format(relay["topic"])) bolForceIdleTimer = True self._plugin_manager.send_plugin_message( self._identifier, dict(topic="{top}".format(**kwargs), relayN="{relayN}".format(**kwargs), currentstate=message.decode("utf-8"))) newrelays.append(relay) if bolRelayStateChanged: self._settings.set(["arrRelays"], newrelays) self._settings.save() if bolForceIdleTimer: self._reset_idle_timer() ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): if event == "WHERE": try: self.mqtt_unsubscribe(self._on_mqtt_subscription) for relay in self._settings.get(["arrRelays"]): self.mqtt_subscribe(self.generate_mqtt_full_topic( relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top=relay["topic"], relayN=relay["relayN"])) except: self._plugin_manager.send_plugin_message( self._identifier, dict(noMQTT=True)) # Client Opened Event if event == Events.CLIENT_OPENED: self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) return # Print Started Event if event == Events.PRINT_STARTED and self.powerOffWhenIdle == True: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._tasmota_mqtt_logger.debug( "Power off aborted because starting new print.") if self._idleTimer is not None: self._reset_idle_timer() self._timeout_value = None self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) # Print Error Event if event == Events.ERROR: self._tasmota_mqtt_logger.debug( "Powering off enabled plugs because there was an error.") for relay in self._settings.get(['arrRelays']): if relay.get("errorEvent", False): self.turn_off(relay) # Timeplapse Events if self.powerOffWhenIdle == True and event == Events.MOVIE_RENDERING: self._tasmota_mqtt_logger.debug( "Timelapse generation started: %s" % payload.get("movie_basename", "")) self._timelapse_active = True if self._timelapse_active and event == Events.MOVIE_DONE or event == Events.MOVIE_FAILED: self._tasmota_mqtt_logger.debug( "Timelapse generation finished: %s. Return Code: %s" % (payload.get("movie_basename", ""), payload.get("returncode", "completed"))) self._timelapse_active = False # Printer Connected Event if event == Events.CONNECTED: if self._autostart_file: self._tasmota_mqtt_logger.debug( "printer connected starting print of %s" % self._autostart_file) self._printer.select_file(self._autostart_file, False, printAfterSelect=True) self._autostart_file = None # File Uploaded Event if event == Events.UPLOAD and any( map(lambda r: r["event_on_upload"] == True, self._settings.get(["arrRelays"]))): if payload.get("print", False): # implemented in OctoPrint version 1.4.1 self._tasmota_mqtt_logger.debug( "File uploaded: %s. Turning enabled relays on." % payload.get("name", "")) self._tasmota_mqtt_logger.debug(payload) for relay in self._settings.get(['arrRelays']): self._tasmota_mqtt_logger.debug(relay) if relay[ "event_on_upload"] is True and not self._printer.is_ready( ): self._tasmota_mqtt_logger.debug( "powering on %s due to %s event." % (relay["topic"], event)) if payload.get( "path", False) and payload.get("target") == "local": self._autostart_file = payload.get("path") self.turn_on(relay) ##~~ AssetPlugin mixin def get_assets(self): return dict(js=[ "js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/fontawesome-iconpicker.js", "js/ko.iconpicker.js", "js/tasmota_mqtt.js" ], css=[ "css/font-awesome.min.css", "css/font-awesome-v4-shims.min.css", "css/fontawesome-iconpicker.css", "css/tasmota_mqtt.css" ]) ##~~ TemplatePlugin mixin def get_template_configs(self): return [ dict(type="navbar", custom_bindings=True), dict(type="settings", custom_bindings=True), dict(type="sidebar", icon="plug", custom_bindings=True, data_bind="visible: filteredSmartplugs().length > 0", template="tasmota_mqtt_sidebar.jinja2", template_header="tasmota_mqtt_sidebar_header.jinja2") ] ##~~ SimpleApiPlugin mixin def get_api_commands(self): return dict(turnOn=["topic", "relayN"], turnOff=["topic", "relayN"], toggleRelay=["topic", "relayN"], checkRelay=["topic", "relayN"], checkStatus=[], removeRelay=["topic", "relayN"], enableAutomaticShutdown=[], disableAutomaticShutdown=[], abortAutomaticShutdown=[], getListPlug=[]) def on_api_command(self, command, data): if not Permissions.PLUGIN_TASMOTA_MQTT_CONTROL.can(): from flask import make_response return make_response("Insufficient rights", 403) if command == 'toggleRelay' or command == 'turnOn' or command == 'turnOff': for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): if command == "turnOff" or (command == "toggleRelay" and relay["currentstate"] == "ON"): self._tasmota_mqtt_logger.debug( "turning off {topic} relay {relayN}".format( **data)) self.turn_off(relay) if command == "turnOn" or (command == "toggleRelay" and relay["currentstate"] == "OFF"): self._tasmota_mqtt_logger.debug( "turning on {topic} relay {relayN}".format(**data)) self.turn_on(relay) if command == 'checkStatus': for relay in self._settings.get(["arrRelays"]): self._tasmota_mqtt_logger.debug( "checking status of %s relay %s" % (relay["topic"], relay["relayN"])) try: self.mqtt_publish( self.generate_mqtt_full_topic(relay, "cmnd"), "") except: self._plugin_manager.send_plugin_message( self._identifier, dict(noMQTT=True)) if command == 'checkRelay': self._tasmota_mqtt_logger.debug( "subscribing to {topic} relay {relayN}".format(**data)) for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): self.mqtt_subscribe( self.generate_mqtt_full_topic(relay, "stat"), self._on_mqtt_subscription, kwargs=dict(top="{topic}".format(**data), relayN="{relayN}".format(**data))) self._tasmota_mqtt_logger.debug( "checking {topic} relay {relayN}".format(**data)) self.mqtt_publish( self.generate_mqtt_full_topic(relay, "cmnd"), "") if command == 'removeRelay': for relay in self._settings.get(["arrRelays"]): if relay["topic"] == "{topic}".format( **data) and relay["relayN"] == "{relayN}".format( **data): self.mqtt_unsubscribe(self._on_mqtt_subscription, topic=self.generate_mqtt_full_topic( relay, "stat")) if command == 'enableAutomaticShutdown': self.powerOffWhenIdle = True self._tasmota_mqtt_logger.debug( "Automatic Power Off enabled, starting idle timer.") self._start_idle_timer() if command == 'disableAutomaticShutdown': self.powerOffWhenIdle = False if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._tasmota_mqtt_logger.debug( "Automatic Power Off disabled, stopping idle and abort timers." ) self._stop_idle_timer() if command == 'abortAutomaticShutdown': if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._timeout_value = None self._tasmota_mqtt_logger.debug("Power off aborted.") self._tasmota_mqtt_logger.debug("Restarting idle timer.") self._reset_idle_timer() if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown": self._tasmota_mqtt_logger.debug( "Automatic power off setting changed: %s" % self.powerOffWhenIdle) self._settings.set_boolean(["powerOffWhenIdle"], self.powerOffWhenIdle) self._settings.save() if command == "enableAutomaticShutdown" or command == "disableAutomaticShutdown" or command == "abortAutomaticShutdown": self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if command == "getListPlug": return json.dumps(self._settings.get(["arrRelays"])) def turn_on(self, relay): self.mqtt_publish(self.generate_mqtt_full_topic(relay, "cmnd"), "ON") if relay["sysCmdOn"]: t = threading.Timer(int(relay["sysCmdOnDelay"]), os.system, args=[relay["sysCmdRunOn"]]) t.start() if relay["connect"] and self._printer.is_closed_or_error(): t = threading.Timer(int(relay["connectOnDelay"]), self._printer.connect) t.start() if self.powerOffWhenIdle == True and relay[ "automaticShutdownEnabled"] == True: self._tasmota_mqtt_logger.debug( "Resetting idle timer since relay %s | %s was just turned on." % (relay["topic"], relay["relayN"])) self._waitForHeaters = False self._reset_idle_timer() def turn_off(self, relay): if relay["sysCmdOff"]: t = threading.Timer(int(relay["sysCmdOffDelay"]), os.system, args=[relay["sysCmdRunOff"]]) t.start() if relay["disconnect"]: self._printer.disconnect() time.sleep(int(relay["disconnectOffDelay"])) self.mqtt_publish(self.generate_mqtt_full_topic(relay, "cmnd"), "OFF") ##~~ Gcode processing hook def gcode_turn_off(self, relay): if relay["warnPrinting"] and self._printer.is_printing(): self._tasmota_mqtt_logger.debug( "Not powering off %s | %s because printer is printing." % (relay["topic"], relay["relayN"])) else: self.turn_off(relay) def processGCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): if gcode: if cmd.startswith("M8") and cmd.count(" ") >= 1: topic = cmd.split()[1] if cmd.count(" ") == 2: relayN = cmd.split()[2] else: relayN = "" for relay in self._settings.get(["arrRelays"]): if relay["topic"].upper() == topic.upper( ) and relay["relayN"] == relayN and relay["gcode"]: if cmd.startswith("M80"): t = threading.Timer(int(relay["gcodeOnDelay"]), self.turn_on, [relay]) t.start() return "M80" elif cmd.startswith("M81"): ## t = threading.Timer(int(relay["gcodeOffDelay"]),self.mqtt_publish,[relay["topic"] + "/cmnd/Power" + relay["relayN"], "OFF"]) t = threading.Timer(int(relay["gcodeOffDelay"]), self.gcode_turn_off, [relay]) t.start() return "M81" elif self.powerOffWhenIdle and not ( gcode in self._idleIgnoreCommandsArray): self._waitForHeaters = False self._reset_idle_timer() return else: return else: return ##~~ Idle Timeout def _start_idle_timer(self): self._stop_idle_timer() if self.powerOffWhenIdle and any( map(lambda r: r["currentstate"] == "ON", self._settings.get(["arrRelays"]))): self._idleTimer = ResettableTimer(self.idleTimeout * 60, self._idle_poweroff) self._idleTimer.start() def _stop_idle_timer(self): if self._idleTimer: self._idleTimer.cancel() self._idleTimer = None def _reset_idle_timer(self): try: if self._idleTimer.is_alive(): self._idleTimer.reset() else: raise Exception() except: self._start_idle_timer() def _idle_poweroff(self): if not self.powerOffWhenIdle: return if self._waitForHeaters: return if self._waitForTimelapse: return if self._printer.is_printing() or self._printer.is_paused(): return if (uptime() / 60) <= (self._settings.get_int(["idleTimeout"])): self._tasmota_mqtt_logger.debug( "Just booted so wait for time sync.") self._tasmota_mqtt_logger.debug( "uptime: {}, comparison: {}".format( (uptime() / 60), (self._settings.get_int(["idleTimeout"])))) self._reset_idle_timer() return self._tasmota_mqtt_logger.debug( "Idle timeout reached after %s minute(s). Turning heaters off prior to powering off plugs." % self.idleTimeout) if self._wait_for_heaters(): self._tasmota_mqtt_logger.debug("Heaters below temperature.") if self._wait_for_timelapse(): self._timer_start() else: self._tasmota_mqtt_logger.debug( "Aborted power off due to activity.") ##~~ Timelapse Monitoring def _wait_for_timelapse(self): self._waitForTimelapse = True self._tasmota_mqtt_logger.debug( "Checking timelapse status before shutting off power...") while True: if not self._waitForTimelapse: return False if not self._timelapse_active: self._waitForTimelapse = False return True self._tasmota_mqtt_logger.debug( "Waiting for timelapse before shutting off power...") time.sleep(5) ##~~ Temperature Cooldown def _wait_for_heaters(self): self._waitForHeaters = True heaters = self._printer.get_current_temperatures() for heater, entry in heaters.items(): target = entry.get("target") if target is None: # heater doesn't exist in fw continue try: temp = float(target) except ValueError: # not a float for some reason, skip it continue if temp != 0: self._tasmota_mqtt_logger.debug("Turning off heater: %s" % heater) self._skipIdleTimer = True self._printer.set_temperature(heater, 0) self._skipIdleTimer = False else: self._tasmota_mqtt_logger.debug("Heater %s already off." % heater) while True: if not self._waitForHeaters: return False heaters = self._printer.get_current_temperatures() highest_temp = 0 heaters_above_waittemp = [] for heater, entry in heaters.items(): if not heater.startswith("tool"): continue actual = entry.get("actual") if actual is None: # heater doesn't exist in fw continue try: temp = float(actual) except ValueError: # not a float for some reason, skip it continue self._tasmota_mqtt_logger.debug("Heater %s = %sC" % (heater, temp)) if temp > self.idleTimeoutWaitTemp: heaters_above_waittemp.append(heater) if temp > highest_temp: highest_temp = temp if highest_temp <= self.idleTimeoutWaitTemp: self._waitForHeaters = False return True self._tasmota_mqtt_logger.debug( "Waiting for heaters(%s) before shutting power off..." % ', '.join(heaters_above_waittemp)) time.sleep(5) ##~~ Abort Power Off Timer def _timer_start(self): if self._abort_timer is not None: return self._tasmota_mqtt_logger.debug("Starting abort power off timer.") self._timeout_value = self.abortTimeout self._abort_timer = RepeatedTimer(1, self._timer_task) self._abort_timer.start() def _timer_task(self): if self._timeout_value is None: return self._timeout_value -= 1 self._plugin_manager.send_plugin_message( self._identifier, dict(powerOffWhenIdle=self.powerOffWhenIdle, type="timeout", timeout_value=self._timeout_value)) if self._timeout_value <= 0: if self._abort_timer is not None: self._abort_timer.cancel() self._abort_timer = None self._shutdown_system() def _shutdown_system(self): self._tasmota_mqtt_logger.debug( "Automatically powering off enabled plugs.") for relay in self._settings.get(['arrRelays']): if relay.get("automaticShutdownEnabled", False): self.turn_off(relay) ##~~ Utility functions def generate_mqtt_full_topic(self, relay, prefix): full_topic = re.sub(r'%topic%', relay["topic"], self._settings.get(["full_topic_pattern"])) full_topic = re.sub(r'%prefix%', prefix, full_topic) full_topic = full_topic + "POWER" + relay["relayN"] return full_topic ##~~ WizardPlugin mixin def is_wizard_required(self): helpers = self._plugin_manager.get_helpers("mqtt") if helpers: return False return True ##~~ Access Permissions Hook def get_additional_permissions(self, *args, **kwargs): return [ dict(key="CONTROL", name="Control Relays", description=gettext("Allows control of configured relays."), roles=["admin"], dangerous=True, default_groups=[ADMIN_GROUP]) ] ##~~ Softwareupdate hook def get_update_information(self): return dict(tasmota_mqtt=dict( displayName="Tasmota-MQTT", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-TasmotaMQTT", current=self._plugin_version, stable_branch=dict( name="Stable", branch="master", comittish=["master"]), prerelease_branches=[ dict( name="Release Candidate", branch="rc", comittish=["rc", "master"], ) ], # update method: pip pip= "https://github.com/jneilliii/OctoPrint-TasmotaMQTT/archive/{target_version}.zip" ))
class SafetyTimeoutPlugin(octoprint.plugin.AssetPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin): def condition(self): ''' This function is to check and see if the temperature and time are set above 0, and a timer does not already exist. If they are: Starts a timer If they are not: It does nothing''' if self.initialstart == True: temperatures = self._printer.get_current_temperatures() if int(self._settings.get(["Time"])) > 0 and temperatures != {}: if float(temperatures.get("bed").get("target")) != 0 or float(temperatures.get("tool0").get("target")) != 0: self.makeTimer() return def makeTimer (self): ''' This function creates a timer instance by grabbing the most recent user specified time''' self.countdowndefined = True self.initial = int(self._settings.get(["Time"])) self._logger.info("The Timer Has Been Initiated!") seconds = self.initial * 60 self.countdown = RepeatedTimer(seconds, self.shutdown, run_first=False) self.initialstart = False self.countdown.start() def on_after_startup(self): ''' Upon server startup, we define the class variables and create our initial timer that constantly checks to see if a safety timeout is needed ''' self.countdowndefined = False self.initialstart = True self.initial = int(self._settings.get(["Time"])) self.timer = RepeatedTimer(1.0, self.condition, run_first=True) self.timer.start() def on_settings_save(self, data): ''' We overload this function in case someone changes the time after a timer has already been set. It will cancel the old timer, and resume checking to see if another one needs to be created''' old_time = self._settings.get(["Time"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_time = self._settings.get(["Time"]) if old_time != new_time: if self.countdowndefined == True: self.countdown.cancel() self._logger.info("The Timer Has Been cancelled!") seconds = int(new_time) * 60 self.initialstart = True def shutdown(self): ''' Checks to see if machine is printing. If it is: Resumes checking to see if a timer is needed. If it is not: Sets the bed to 0''' self.countdown.cancel() self.countdowndefined = False #check to see if the printer is printing if self._printer.is_printing(): # print("The printer is printing!") self.initialstart = True # print("self.countdown has been cancelled") #if the printer is printing we do not want to interupt it else: #else, we need to set the temperature of the bed and the tool to 0 self._printer.set_temperature("bed", 0) # print("Bed temperature set to 0") self._printer.set_temperature("tool0", 0) # print("Tool temperature set to 0") self.initialstart = True # print("self.countdown has been cancelled") def get_settings_defaults(self): #set the default time return dict(Time="10") def get_template_configs(self): return [ dict(type="settings", custom_bindings=False) ]