diff --git a/src/octoprint/events.py b/src/octoprint/events.py index f9216b06..6b332454 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -203,36 +203,42 @@ class DebugEventListener(GenericEventListener): class CommandTrigger(GenericEventListener): - def __init__(self, triggerType, printer): + def __init__(self, printer): GenericEventListener.__init__(self) self._printer = printer self._subscriptions = {} - self._initSubscriptions(triggerType) + self._initSubscriptions() - def _initSubscriptions(self, triggerType): + def _initSubscriptions(self): """ Subscribes all events as defined in "events > $triggerType > subscriptions" in the settings with their respective commands. """ - if not settings().get(["events", triggerType]): + if not settings().get(["events"]): return - if not settings().getBoolean(["events", triggerType, "enabled"]): + if not settings().getBoolean(["events", "enabled"]): return eventsToSubscribe = [] - for subscription in settings().get(["events", triggerType, "subscriptions"]): - if not "event" in subscription.keys() or not "command" in subscription.keys(): - self._logger.info("Invalid %s, missing either event or command: %r" % (triggerType, subscription)) + for subscription in settings().get(["events", "subscriptions"]): + if not "event" in subscription.keys() or not "command" in subscription.keys() \ + or not "type" in subscription.keys() or not subscription["type"] in ["system", "gcode"]: + self._logger.info("Invalid command trigger, missing either event, type or command or type is invalid: %r" % subscription) + continue + + if "enabled" in subscription.keys() and not subscription["enabled"]: + self._logger.info("Disabled command trigger: %r" % subscription) continue event = subscription["event"] command = subscription["command"] + commandType = subscription["type"] if not event in self._subscriptions.keys(): self._subscriptions[event] = [] - self._subscriptions[event].append(command) + self._subscriptions[event].append((command, commandType)) if not event in eventsToSubscribe: eventsToSubscribe.append(event) @@ -250,18 +256,48 @@ class CommandTrigger(GenericEventListener): if not event in self._subscriptions: return - for command in self._subscriptions[event]: + for command, commandType in self._subscriptions[event]: try: - processedCommand = self._processCommand(command, payload) - self.executeCommand(processedCommand) + if isinstance(command, (tuple, list, set)): + processedCommand = [] + for c in command: + processedCommand.append(self._processCommand(c, payload)) + else: + processedCommand = self._processCommand(command, payload) + self.executeCommand(processedCommand, commandType) except KeyError, e: self._logger.warn("There was an error processing one or more placeholders in the following command: %s" % command) - def executeCommand(self, command): - """ - Not implemented, override in child classes - """ - pass + def executeCommand(self, command, commandType): + if commandType == "system": + self._executeSystemCommand(command) + elif commandType == "gcode": + self._executeGcodeCommand(command) + + def _executeSystemCommand(self, command): + def commandExecutioner(command): + self._logger.info("Executing system command: %s" % command) + subprocess.Popen(command, shell=True) + + try: + if isinstance(command, (list, tuple, set)): + for c in command: + commandExecutioner(c) + else: + commandExecutioner(command) + except subprocess.CalledProcessError, e: + self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + except Exception, ex: + self._logger.exception("Command failed") + + def _executeGcodeCommand(self, command): + commands = [command] + if isinstance(command, (list, tuple, set)): + self.logger.debug("Executing GCode commands: %r" % command) + commands = list(command) + else: + self._logger.debug("Executing GCode command: %s" % command) + self._printer.commands(commands) def _processCommand(self, command, payload): """ @@ -302,34 +338,3 @@ class CommandTrigger(GenericEventListener): params.update(payload) return command.format(**params) - - -class SystemCommandTrigger(CommandTrigger): - """ - Performs configured system commands for configured events. - """ - - def __init__(self, printer): - CommandTrigger.__init__(self, "systemCommandTrigger", printer) - - def executeCommand(self, command): - try: - self._logger.info("Executing system command: %s" % command) - subprocess.Popen(command, shell=True) - except subprocess.CalledProcessError, e: - self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) - except Exception, ex: - self._logger.exception("Command failed") - - -class GcodeCommandTrigger(CommandTrigger): - """ - Sends configured GCODE commands to the printer for configured events. - """ - - def __init__(self, printer): - CommandTrigger.__init__(self, "gcodeCommandTrigger", printer) - - def executeCommand(self, command): - self._logger.debug("Executing GCode command: %s" % command) - self._printer.commands(command.split(",")) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 28f186cc..692bc5f0 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -135,9 +135,8 @@ class Server(): # configure timelapse octoprint.timelapse.configureTimelapse() - # setup system and gcode command triggers - events.SystemCommandTrigger(printer) - events.GcodeCommandTrigger(printer) + # setup command triggers + events.CommandTrigger(printer) if self._debug: events.DebugEventListener() diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index e116f652..4a724138 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -125,12 +125,8 @@ default_settings = { "config": "/default/path/to/your/cura/config.ini" }, "events": { - "systemCommandTrigger": { - "enabled": False - }, - "gcodeCommandTrigger": { - "enabled": False - } + "enabled": False, + "subscriptions": [] }, "api": { "enabled": False, @@ -173,7 +169,7 @@ class Settings(object): self._configfile = configfile else: self._configfile = os.path.join(self.settings_dir, "config.yaml") - self.load() + self.load(migrate=True) def _init_settings_dir(self, basedir): if basedir is not None: @@ -189,7 +185,7 @@ class Settings(object): #~~ load and save - def load(self): + def load(self, migrate=False): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: self._config = yaml.safe_load(f) @@ -197,6 +193,97 @@ class Settings(object): if not self._config: self._config = {} + if migrate: + self._migrateConfig() + + def _migrateConfig(self): + if not self._config: + return + + if "events" in self._config.keys() and ("gcodeCommandTrigger" in self._config["events"] or "systemCommandTrigger" in self._config["events"]): + self._logger.info("Migrating config (event subscriptions)...") + + # migrate event hooks to new format + placeholderRe = re.compile("%\((.*?)\)s") + + eventNameReplacements = { + "ClientOpen": "ClientOpened", + "TransferStart": "TransferStarted" + } + payloadDataReplacements = { + "Upload": {"data": "{file}", "filename": "{file}"}, + "Connected": {"data": "{port} at {baudrate} baud"}, + "FileSelected": {"data": "{file}", "filename": "{file}"}, + "TransferStarted": {"data": "{remote}", "filename": "{remote}"}, + "TransferDone": {"data": "{remote}", "filename": "{remote}"}, + "ZChange": {"data": "{new}"}, + "CaptureStart": {"data": "{file}"}, + "CaptureDone": {"data": "{file}"}, + "MovieDone": {"data": "{movie}", "filename": "{gcode}"}, + "Error": {"data": "{error}"}, + "PrintStarted": {"data": "{file}", "filename": "{file}"}, + "PrintDone": {"data": "{file}", "filename": "{file}"}, + } + + def migrateEventHook(event, command): + # migrate placeholders + command = placeholderRe.sub("{__\\1}", command) + + # migrate event names + if event in eventNameReplacements: + event = eventNameReplacements["event"] + + # migrate payloads to more specific placeholders + if event in payloadDataReplacements: + for key in payloadDataReplacements[event]: + command = command.replace("{__%s}" % key, payloadDataReplacements[event][key]) + + # return processed tuple + return event, command + + disableSystemCommands = False + if "systemCommandTrigger" in self._config["events"] and "enabled" in self._config["events"]["systemCommandTrigger"]: + disableSystemCommands = not self._config["events"]["systemCommandTrigger"]["enabled"] + + disableGcodeCommands = False + if "gcodeCommandTrigger" in self._config["events"] and "enabled" in self._config["events"]["gcodeCommandTrigger"]: + disableGcodeCommands = not self._config["events"]["gcodeCommandTrigger"]["enabled"] + + disableAllCommands = disableSystemCommands and disableGcodeCommands + newEvents = { + "enabled": not disableAllCommands, + "subscriptions": [] + } + + if "systemCommandTrigger" in self._config["events"] and "subscriptions" in self._config["events"]["systemCommandTrigger"]: + for trigger in self._config["events"]["systemCommandTrigger"]["subscriptions"]: + if not ("event" in trigger and "command" in trigger): + continue + + newTrigger = {"type": "system"} + if disableSystemCommands and not disableAllCommands: + newTrigger["enabled"] = False + + newTrigger["event"], newTrigger["command"] = migrateEventHook(trigger["event"], trigger["command"]) + newEvents["subscriptions"].append(newTrigger) + + if "gcodeCommandTrigger" in self._config["events"] and "subscriptions" in self._config["events"]["gcodeCommandTrigger"]: + for trigger in self._config["events"]["gcodeCommandTrigger"]["subscriptions"]: + if not ("event" in trigger and "command" in trigger): + continue + + newTrigger = {"type": "gcode"} + if disableGcodeCommands and not disableAllCommands: + newTrigger["enabled"] = False + + newTrigger["event"], newTrigger["command"] = migrateEventHook(trigger["event"], trigger["command"]) + newTrigger["command"] = newTrigger["command"].split(",") + newEvents["subscriptions"].append(newTrigger) + + self._config["events"] = newEvents + self.save(force=True) + self._logger.info("Migrated %d event subscriptions to new format and structure" % len(newEvents["subscriptions"])) + def save(self, force=False): if not self._dirty and not force: return