def on_after_startup(self): # Use a different log file for DiscordRemote, as it is very noisy. self._logger = logging.getLogger("octoprint.plugins.discordremote") from octoprint.logging.handlers import CleaningTimedRotatingFileHandler hdlr = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(), when="D", backupCount=3) formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) self._logger.addHandler(hdlr) # Initialise DiscordRemote self._logger.info("DiscordRemote is started !") if self.command is None: self.command = Command(self) # Configure discord if self.discord is None: self.discord = Discord() self.discord.configure_discord(self._settings.get(['bottoken'], merged=True), self._settings.get(['channelid'], merged=True), self._logger, self.command, self.update_discord_status) # Transition settings allowed_users = self._settings.get(['allowedusers'], merged=True) if allowed_users: self._settings.set(["allowedusers"], None, True) self._settings.set(['permissions'], {'1': {'users': allowed_users, 'commands': ''}}, True) self.send_message(None, "⚠️⚠️⚠️ Allowed users has been changed to a more granular system. " "Check the DiscordRemote settings and check that it is suitable⚠️⚠️⚠️")
def configure_discord(self, send_test=False): # Configure discord if self.command is None: self.command = Command(self) if self.discord is None: self.discord = Discord() self.discord.configure_discord( self._settings.get(['bottoken'], merged=True), self._settings.get(['channelid'], merged=True), self._logger, self.command, self.update_discord_status) if send_test: self.notify_event("test")
def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._logger.info("Settings have saved. Send a test message...") # Configure discord if self.command is None: self.command = Command(self) if self.discord is None: self.discord = Discord() self.discord.configure_discord(self._settings.get(['bottoken'], merged=True), self._settings.get(['channelid'], merged=True), self._logger, self.command, self.update_discord_status) self.notify_event("test")
def configure_discord(self, send_test=False): # Configure discord if self.command is None: self.command = Command(self) if self.discord: self.discord.shutdown_discord() self.discord = DiscordImpl( self._settings.get(['bottoken'], merged=True), self._settings.get(['channelid'], merged=True), self._logger, self.command, self.update_discord_status) if self.presence is None: self.presence = Presence() self.presence.configure_presence(self, self.discord) self.notify_event("startup") if send_test: self.notify_event("test")
class DiscordRemotePlugin(octoprint.plugin.EventHandlerPlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.ShutdownPlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.ProgressPlugin, octoprint.plugin.SimpleApiPlugin): def __init__(self): self.discord = None self.command = None self.last_progress_message = None self.last_progress_percent = 0 self.is_muted = False self.periodic_signal = None self.periodic_thread = None # Events definition here (better for intellisense in IDE) # referenced in the settings too. self.events = { "startup": { "name": "Octoprint Startup", "enabled": True, "with_snapshot": False, "message": "⏰ I just woke up! What are we gonna print today?\n" "Local IP: {ipaddr} External IP: {externaddr}" }, "shutdown": { "name": "Octoprint Shutdown", "enabled": True, "with_snapshot": False, "message": "💤 Going to bed now!" }, "printer_state_operational": { "name": "Printer state : operational", "enabled": True, "with_snapshot": False, "message": "✅ Your printer is operational." }, "printer_state_error": { "name": "Printer state : error", "enabled": True, "with_snapshot": False, "message": "⚠️ Your printer is in an erroneous state." }, "printer_state_unknown": { "name": "Printer state : unknown", "enabled": True, "with_snapshot": False, "message": "❔ Your printer is in an unknown state." }, "printing_started": { "name": "Printing process : started", "enabled": True, "with_snapshot": True, "message": "🖨️ I've started printing {path}" }, "printing_paused": { "name": "Printing process : paused", "enabled": True, "with_snapshot": True, "message": "⏸️ The printing was paused." }, "printing_resumed": { "name": "Printing process : resumed", "enabled": True, "with_snapshot": True, "message": "▶️ The printing was resumed." }, "printing_cancelled": { "name": "Printing process : cancelled", "enabled": True, "with_snapshot": True, "message": "🛑 The printing was stopped." }, "printing_done": { "name": "Printing process : done", "enabled": True, "with_snapshot": True, "message": "👍 Printing is done! Took about {time_formatted}" }, "printing_failed": { "name": "Printing process : failed", "enabled": True, "with_snapshot": True, "message": "👎 Printing has failed! :(" }, "printing_progress": { "name": "Printing progress (Percentage)", "enabled": True, "with_snapshot": True, "message": "📢 Printing is at {progress}%", "step": 10 }, "printing_progress_periodic": { "name": "Printing progress (Periodic)", "enabled": False, "with_snapshot": True, "message": "📢 Printing is at {progress}%", "period": 300 }, "test": { # Not a real message, but we will treat it as one "enabled": True, "with_snapshot": True, "message": "Hello hello! If you see this message, it means that the settings are correct!" }, } self.permissions = { '1': {'users': '*', 'commands': ''}, '2': {'users': '', 'commands': ''}, '3': {'users': '', 'commands': ''}, '4': {'users': '', 'commands': ''}, '5': {'users': '', 'commands': ''} } def configure_discord(self, send_test=False): # Configure discord if self.command is None: self.command = Command(self) if self.discord is None: self.discord = Discord() self.discord.configure_discord(self._settings.get(['bottoken'], merged=True), self._settings.get(['channelid'], merged=True), self._logger, self.command, self.update_discord_status) if send_test: self.notify_event("test") def on_after_startup(self): # Use a different log file for DiscordRemote, as it is very noisy. self._logger = logging.getLogger("octoprint.plugins.discordremote") from octoprint.logging.handlers import CleaningTimedRotatingFileHandler hdlr = CleaningTimedRotatingFileHandler( self._settings.get_plugin_logfile_path(), when="D", backupCount=3) formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) self._logger.addHandler(hdlr) # Initialise DiscordRemote self._logger.info("DiscordRemote is started !") self.configure_discord(False) # Transition settings allowed_users = self._settings.get(['allowedusers'], merged=True) if allowed_users: self._settings.set(["allowedusers"], None, True) self._settings.set(['permissions'], {'1': {'users': allowed_users, 'commands': ''}}, True) self.send_message(None, "⚠️⚠️⚠️ Allowed users has been changed to a more granular system. " "Check the DiscordRemote settings and check that it is suitable⚠️⚠️⚠️") # ShutdownPlugin mixin def on_shutdown(self): self._logger.info("DiscordRemote is shutting down.") self.discord.shutdown_discord() self._logger.info("Discord bot has excited cleanly.") # SettingsPlugin mixin def get_settings_defaults(self): return { 'bottoken': "", 'channelid': "", 'baseurl': "", 'prefix': "/", 'show_local_ip': 'auto', 'show_external_ip': 'auto', 'use_hostname': False, 'hostname': "YOUR.HOST.NAME", 'use_hostname_only': False, 'events': self.events, 'permissions': self.permissions, 'allow_scripts': False, 'script_before': '', 'script_after': '', 'allowed_gcode': '' } # Restricts some paths to some roles only def get_settings_restricted_paths(self): # settings.events.tests is a false message, so we should never see it as configurable. # settings.bottoken and channelid are admin only. return dict(never=[["events", "test"]], admin=[["bottoken"], ["channelid"], ["permissions"], ['baseurl'], ['prefix'], ["show_local_ip"], ["show_external_ip"], ["use_hostname"], ["hostname"], ["use_hostname_only"], ['script_before'], ['script_after'], ['allowed_gcode']]) # AssetPlugin mixin def get_assets(self): # Define your plugin's asset files to automatically include in the # core UI here. return dict( js=["js/discordremote.js"], css=["css/discordremote.css"], less=["less/discordremote.less"] ) # TemplatePlugin mixin def get_template_configs(self): return [ dict(type="settings", custom_bindings=False) ] # 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( discordremote=dict( displayName="DiscordRemote Plugin", displayVersion=self._plugin_version, # version check: github repository type="github_release", user="******", repo="OctoPrint-DiscordRemote", current=self._plugin_version, # update method: pip pip="https://github.com/cameroncros/OctoPrint-DiscordRemote/archive/{target_version}.zip" ) ) # EventHandlerPlugin hook def on_event(self, event, payload): if event == "Startup": return self.notify_event("startup") if event == "Shutdown": return self.notify_event("shutdown") if event == "PrinterStateChanged": if payload["state_id"] == "OPERATIONAL": return self.notify_event("printer_state_operational") elif payload["state_id"] == "ERROR": return self.notify_event("printer_state_error") elif payload["state_id"] == "UNKNOWN": return self.notify_event("printer_state_unknown") if event == "PrintStarted": self.start_periodic_reporting() return self.notify_event("printing_started", payload) if event == "PrintPaused": return self.notify_event("printing_paused", payload) if event == "PrintResumed": return self.notify_event("printing_resumed", payload) if event == "PrintCancelled": return self.notify_event("printing_cancelled", payload) if event == "PrintDone": self.stop_periodic_reporting() payload['time_formatted'] = timedelta(seconds=int(payload["time"])) return self.notify_event("printing_done", payload) return True def on_print_progress(self, location, path, progress): # Avoid sending duplicate percentage progress messages if progress != self.last_progress_percent: self.last_progress_percent = progress self.notify_event("printing_progress", {"progress": progress}) def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._logger.info("Settings have saved. Send a test message...") thread = threading.Thread(target=self.configure_discord, args=(True,)) thread.start() # SimpleApiPlugin mixin def get_api_commands(self): return dict( executeCommand=['args'], sendMessage=[] ) def on_api_command(self, comm, data): if not user_permission.can(): return make_response("Insufficient rights", 403) if comm == 'executeCommand': return self.execute_command(data) if comm == 'sendMessage': return self.unpack_message(data) def execute_command(self, data): args = "" if 'args' in data: args = data['args'] snapshots, embeds = self.command.parse_command(args) if not self.discord.send(snapshots=snapshots, embeds=embeds): return make_response("Failed to send message", 404) def unpack_message(self, data): builder = embedbuilder.EmbedBuilder() if 'title' in data: builder.set_title(data['title']) if 'author' in data: builder.set_author(data['author']) if 'color' in data: builder.set_color(data['color']) if 'description' in data: builder.set_description(data['description']) if 'image' in data: b64image = data['image'] imagename = data.get('imagename', 'snapshot.png') bytes = b64decode(b64image) image = BytesIO(bytes) builder.set_image((imagename, image)) if not self.discord.send(embeds=builder.get_embeds()): return make_response("Failed to send message", 404) def notify_event(self, event_id, data=None): self._logger.info("Received event: %s" % event_id) if self.is_muted: return True if data is None: data = {} if event_id not in self.events: self._logger.error("Tried to notify on non-existant eventID : ", event_id) return False tmp_config = self._settings.get(["events", event_id], merged=True) if not tmp_config["enabled"]: self._logger.debug("Event {} is not enabled. Returning gracefully".format(event_id)) return False # Store IP address for message data['ipaddr'] = self.get_ip_address() data['externaddr'] = self.get_external_ip_address() data['timeremaining'] = self.get_print_time_remaining() data['timespent'] = self.get_print_time_spent() # Special case for progress eventID : we check for progress and steps if event_id == 'printing_progress': # Skip if just started if int(data["progress"]) == 0: return False # Skip if not a multiple of the given interval if int(data["progress"]) % int(tmp_config["step"]) != 0: return False # Always send last message, and reset timer. if int(data["progress"]) == 100: self.last_progress_message = None done_config = self._settings.get(["events", "printing_done"], merged=True) # Don't send last message if the "printing_done" event is enabled. if done_config["enabled"]: return False # Otherwise work out if time since last message has passed. try: min_progress_time = timedelta(seconds=int(tmp_config["timeout"])) if self.last_progress_message is not None \ and self.last_progress_message > (datetime.now() - min_progress_time): return False except ValueError: pass except KeyError: pass self.last_progress_message = datetime.now() return self.send_message(event_id, tmp_config["message"].format(**data), tmp_config["with_snapshot"]) def get_ip_address(self): if self._settings.get(['show_local_ip'], merged=True) == 'hostname': return self._settings.get(['hostname'], merged=True) elif self._settings.get(['show_local_ip'], merged=True) == 'auto': s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # doesn't even have to be reachable s.connect(('10.255.255.255', 1)) return s.getsockname()[0] except Exception as e: print(e) return '127.0.0.1' finally: s.close() else: return None def get_external_ip_address(self): if self._settings.get(['show_local_ip'], merged=True) == 'hostname': return self._settings.get(['hostname'], merged=True) elif self._settings.get(['show_local_ip'], merged=True) == 'auto': return ipgetter.myip() else: return None def get_port(self): port = self.get_settings().global_get(["plugins", "discovery", "publicPort"]) if port: return port port = self.get_settings().global_get(["server", "port"]) if port: return port return 5000 # Default to a sane value def exec_script(self, event_name, which=""): # I want to be sure that the scripts are allowed by the special configuration flag scripts_allowed = self._settings.get(["allow_scripts"], merged=True) if scripts_allowed is None or scripts_allowed is False: return "" # Finding which one should be used. script_to_exec = None if which == "before": script_to_exec = self._settings.get(["script_before"], merged=True) elif which == "after": script_to_exec = self._settings.get(["script_after"], merged=True) # Finally exec the script out = "" self._logger.info("{}:{} File to start: '{}'".format(event_name, which, script_to_exec)) try: if script_to_exec is not None and len(script_to_exec) > 0 and os.path.exists(script_to_exec): out = subprocess.check_output(script_to_exec) except (OSError, subprocess.CalledProcessError) as err: out = err finally: self._logger.info("{}:{} > Output: '{}'".format(event_name, which, out)) return out def send_message(self, event_id, message, with_snapshot=False): # exec "before" script if any self.exec_script(event_id, "before") # Get snapshot if asked for snapshot = None if with_snapshot: snapshots = self.get_snapshot() if snapshots and len(snapshots) == 1: snapshot = snapshots[0] # Send to Discord bot (Somehow events can happen before discord bot has been created and initialised) if self.discord is None: self.discord = Discord() out = self.discord.send(embeds=info_embed(author=self.get_printer_name(), title=message, snapshot=snapshot)) if not out: self._logger.error("Failed to send message") return out # exec "after" script if any self.exec_script(event_id, "after") return out def get_snapshot(self): if 'FAKE_SNAPSHOT' in os.environ: return self.get_snapshot_fake() else: return self.get_snapshot_camera() @staticmethod def get_snapshot_fake(): fl = open(os.environ['FAKE_SNAPSHOT']) return [("snapshot.png", fl)] def get_snapshot_camera(self): snapshot = None snapshot_url = self._settings.global_get(["webcam", "snapshot"]) if snapshot_url is None: return None if "http" in snapshot_url: try: snapshot_call = requests.get(snapshot_url) if not snapshot_call: return None snapshot = BytesIO(snapshot_call.content) except ConnectionError: return None if snapshot_url.startswith("file://"): snapshot = open(snapshot_url.partition('file://')[2], "rb") if snapshot is None: return None # Get the settings used for streaming to know if we should transform the snapshot must_flip_h = self._settings.global_get_boolean(["webcam", "flipH"]) must_flip_v = self._settings.global_get_boolean(["webcam", "flipV"]) must_rotate = self._settings.global_get_boolean(["webcam", "rotate90"]) # Only call Pillow if we need to transpose anything if must_flip_h or must_flip_v or must_rotate: img = Image.open(snapshot) self._logger.info( "Transformations : FlipH={}, FlipV={} Rotate={}".format(must_flip_h, must_flip_v, must_rotate)) if must_flip_h: img = img.transpose(Image.FLIP_LEFT_RIGHT) if must_flip_v: img = img.transpose(Image.FLIP_TOP_BOTTOM) if must_rotate: img = img.transpose(Image.ROTATE_90) new_image = BytesIO() img.save(new_image, 'png') return [("snapshot.png", new_image)] return [("snapshot.png", snapshot)] def get_printer_name(self): printer_name = self._settings.global_get(["appearance", "name"]) if printer_name is None: printer_name = "OctoPrint" return printer_name def update_discord_status(self, connected): self._plugin_manager.send_plugin_message(self._identifier, dict(isConnected=connected)) def mute(self): self.is_muted = True def unmute(self): self.is_muted = False def get_file_manager(self): return self._file_manager def get_settings(self): return self._settings def get_printer(self): return self._printer def get_plugin_manager(self): return self._plugin_manager def get_print_time_spent(self): current_data = self._printer.get_current_data() try: current_time_val = current_data['progress']['printTime'] return humanfriendly.format_timespan(current_time_val, max_units=2) except (KeyError, ValueError): return 'Unknown' def get_print_time_remaining(self): current_data = self._printer.get_current_data() try: remaining_time_val = current_data['progress']['printTimeLeft'] return humanfriendly.format_timespan(remaining_time_val, max_units=2) except (KeyError, ValueError): return 'Unknown' def start_periodic_reporting(self): self.stop_periodic_reporting() self.last_progress_percent = 0 self.periodic_signal = Event() self.periodic_signal.clear() self.periodic_thread = Thread(target=self.periodic_reporting) self.periodic_thread.start() def stop_periodic_reporting(self): if self.periodic_signal is None or self.periodic_thread is None: return self.periodic_signal.set() self.periodic_thread.join(timeout=60) if self.periodic_thread.is_alive(): self._logger.error("Periodic thread has hung, leaking it now.") else: self._logger.info("Periodic thread joined.") self.periodic_thread = None self.periodic_signal = None def periodic_reporting(self): if not self._settings.get(["events", "printing_progress_periodic", "enabled"]): return timeout = self._settings.get(["events", "printing_progress_periodic", "period"]) while True: cur_time = time.time() next_time = cur_time + int(timeout) while time.time() < next_time: time.sleep(1) if self.periodic_signal.is_set(): return if not self._printer.is_printing(): return self.notify_event("printing_progress_periodic", data={"progress": self.last_progress_percent})