def __init__(self, config: ConfigHelper) -> None: name_parts = config.get_name().split(maxsplit=1) if len(name_parts) != 2: raise config.error(f"Invalid Section Name: {config.get_name()}") self.server = config.get_server() self.name = name_parts[1] self.type: str = config.get('type') self.state: str = "init" self.locked_while_printing = config.getboolean('locked_while_printing', False) self.off_when_shutdown = config.getboolean('off_when_shutdown', False) self.off_when_shutdown_delay = 0. if self.off_when_shutdown: self.off_when_shutdown_delay = config.getfloat( 'off_when_shutdown_delay', 0., minval=0.) self.shutdown_timer_handle: Optional[asyncio.TimerHandle] = None self.restart_delay = 1. self.klipper_restart = config.getboolean( 'restart_klipper_when_powered', False) if self.klipper_restart: self.restart_delay = config.getfloat('restart_delay', 1.) if self.restart_delay < .000001: raise config.error("Option 'restart_delay' must be above 0.0") self.bound_service: Optional[str] = config.get('bound_service', None) self.need_scheduled_restart = False self.on_when_queued = config.getboolean('on_when_upload_queued', False)
def __init__(self, config: ConfigHelper) -> None: super().__init__(config) self.mqtt: MQTTClient = self.server.load_component(config, 'mqtt') self.eventloop = self.server.get_event_loop() self.cmd_topic: str = config.get('command_topic') self.cmd_payload: JinjaTemplate = config.gettemplate('command_payload') self.retain_cmd_state = config.getboolean('retain_command_state', False) self.query_topic: Optional[str] = config.get('query_topic', None) self.query_payload = config.gettemplate('query_payload', None) self.must_query = config.getboolean('query_after_command', False) if self.query_topic is not None: self.must_query = False self.state_topic: str = config.get('state_topic') self.state_timeout = config.getfloat('state_timeout', 2.) self.state_response = config.load_template('state_response_template', "{payload}") self.qos: Optional[int] = config.getint('qos', None, minval=0, maxval=2) self.mqtt.subscribe_topic(self.state_topic, self._on_state_update, self.qos) self.query_response: Optional[asyncio.Future] = None self.request_mutex = asyncio.Lock() self.server.register_event_handler("mqtt:connected", self._on_mqtt_connected) self.server.register_event_handler("mqtt:disconnected", self._on_mqtt_disconnected)
def test_get_int_deprecate(self, test_config: ConfigHelper): server = test_config.get_server() test_config.getboolean("test_bool", deprecate=True) expected = ( f"[test_options]: Option 'test_bool' is " "deprecated, see the configuration documention " "at https://moonraker.readthedocs.io/en/latest/configuration") assert expected in server.warnings
def from_config(cls, config: ConfigHelper) -> WebCam: webcam: Dict[str, Any] = {} webcam["name"] = config.get_name().split(maxsplit=1)[-1] webcam["location"] = config.get("location", "printer") webcam["service"] = config.get("service", "mjpegstreamer") webcam["target_fps"] = config.getint("target_fps", 15) webcam["stream_url"] = config.get("stream_url") webcam["snapshot_url"] = config.get("snapshot_url") webcam["flip_horizontal"] = config.getboolean("flip_horizontal", False) webcam["flip_vertical"] = config.getboolean("flip_vertical", False) webcam["rotation"] = config.getint("rotation", 0) if webcam["rotation"] not in [0, 90, 180, 270]: raise config.error("Invalid value for option 'rotation'") webcam["source"] = "config" return cls(config.get_server(), **webcam)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.debug_enabled = config.getboolean('enable_repo_debug', False) if self.debug_enabled: logging.warning("UPDATE MANAGER: REPO DEBUG ENABLED") shell_cmd: SCMDComp = self.server.lookup_component('shell_command') self.scmd_error = shell_cmd.error self.build_shell_command = shell_cmd.build_shell_command self.pkg_updater: Optional[PackageDeploy] = None self.http_client = AsyncHTTPClient() self.github_request_cache: Dict[str, CachedGithubResponse] = {} # database management db: DBComp = self.server.lookup_component('database') db.register_local_namespace("update_manager") self.umdb = db.wrap_namespace("update_manager") # Refresh Time Tracking (default is to refresh every 28 days) reresh_interval = config.getint('refresh_interval', 672) # Convert to seconds self.refresh_interval = reresh_interval * 60 * 60 # GitHub API Rate Limit Tracking self.gh_rate_limit: Optional[int] = None self.gh_limit_remaining: Optional[int] = None self.gh_limit_reset_time: Optional[float] = None # Update In Progress Tracking self.cur_update_app: Optional[str] = None self.cur_update_id: Optional[int] = None self.full_complete: bool = False
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.namespaces: Dict[str, object] = {} self.enable_debug = config.getboolean("enable_database_debug", False) self.database_path = os.path.expanduser( config.get('database_path', "~/.moonraker_database")) if not os.path.isdir(self.database_path): os.mkdir(self.database_path) self.lmdb_env = lmdb.open(self.database_path, map_size=MAX_DB_SIZE, max_dbs=MAX_NAMESPACES) with self.lmdb_env.begin(write=True, buffers=True) as txn: # lookup existing namespaces cursor = txn.cursor() remaining = cursor.first() while remaining: key = bytes(cursor.key()) self.namespaces[key.decode()] = self.lmdb_env.open_db(key, txn) remaining = cursor.next() cursor.close() if "moonraker" not in self.namespaces: mrdb = self.lmdb_env.open_db(b"moonraker", txn) self.namespaces["moonraker"] = mrdb txn.put(b'database_version', self._encode_value(DATABASE_VERSION), db=mrdb) # Read out all namespaces to remove any invalid keys on init for ns in self.namespaces.keys(): self._get_namespace(ns) # Protected Namespaces have read-only API access. Write access can # be granted by enabling the debug option. Forbidden namespaces # have no API access. This cannot be overridden. self.protected_namespaces = set( self.get_item("moonraker", "database.protected_namespaces", ["moonraker"])) self.forbidden_namespaces = set( self.get_item("moonraker", "database.forbidden_namespaces", [])) debug_counter: int = self.get_item("moonraker", "database.debug_counter", 0) if self.enable_debug: debug_counter += 1 self.insert_item("moonraker", "database.debug_counter", debug_counter) if debug_counter: logging.info(f"Database Debug Count: {debug_counter}") # Track unsafe shutdowns unsafe_shutdowns: int = self.get_item("moonraker", "database.unsafe_shutdowns", 0) logging.info(f"Unsafe Shutdown Count: {unsafe_shutdowns}") # Increment unsafe shutdown counter. This will be reset if # moonraker is safely restarted self.insert_item("moonraker", "database.unsafe_shutdowns", unsafe_shutdowns + 1) self.server.register_endpoint("/server/database/list", ['GET'], self._handle_list_request) self.server.register_endpoint("/server/database/item", ["GET", "POST", "DELETE"], self._handle_item_request)
def __init__(self, config: ConfigHelper) -> None: name_parts = config.get_name().split(maxsplit=1) if len(name_parts) != 2: raise config.error(f"Invalid Section Name: {config.get_name()}") self.server = config.get_server() self.name = name_parts[1] self.type: str = config.get('type') self.state: str = "init" self.locked_while_printing = config.getboolean('locked_while_printing', False) self.off_when_shutdown = config.getboolean('off_when_shutdown', False) self.restart_delay = 1. self.klipper_restart = config.getboolean( 'restart_klipper_when_powered', False) if self.klipper_restart: self.restart_delay = config.getfloat('restart_delay', 1.) if self.restart_delay < .000001: raise config.error("Option 'restart_delay' must be above 0.0")
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.entry_mgr = EntryManager(config) self.eventloop = self.server.get_event_loop() self.update_timer = self.eventloop.register_timer( self._handle_update_timer ) self.request_lock = asyncio.Lock() self.dev_mode = config.getboolean("dev_mode", False) self.subscriptions: Dict[str, RssFeed] = { "moonraker": RssFeed("moonraker", self.entry_mgr, self.dev_mode), "klipper": RssFeed("klipper", self.entry_mgr, self.dev_mode) } self.stored_feeds: List[str] = [] sub_list: List[str] = config.getlist("subscriptions", []) self.configured_feeds: List[str] = ["moonraker", "klipper"] for sub in sub_list: sub = sub.lower() if sub in self.subscriptions: continue self.configured_feeds.append(sub) self.subscriptions[sub] = RssFeed( sub, self.entry_mgr, self.dev_mode ) self.server.register_endpoint( "/server/announcements/list", ["GET"], self._list_announcements ) self.server.register_endpoint( "/server/announcements/dismiss", ["POST"], self._handle_dismiss_request ) self.server.register_endpoint( "/server/announcements/update", ["POST"], self._handle_update_request ) self.server.register_endpoint( "/server/announcements/feed", ["POST", "DELETE"], self._handle_feed_request ) self.server.register_endpoint( "/server/announcements/feeds", ["GET"], self._handle_list_feeds ) self.server.register_notification( "announcements:dismissed", "announcement_dismissed" ) self.server.register_notification( "announcements:entries_updated", "announcement_update" ) self.server.register_notification( "announcements:dismiss_wake", "announcement_wake" )
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.http_server: Optional[HTTPServer] = None self.secure_server: Optional[HTTPServer] = None self.api_cache: Dict[str, APIDefinition] = {} self.registered_base_handlers: List[str] = [] self.max_upload_size = config.getint('max_upload_size', 1024) self.max_upload_size *= 1024 * 1024 # SSL config self.cert_path: str = self._get_path_option( config, 'ssl_certificate_path') self.key_path: str = self._get_path_option( config, 'ssl_key_path') # Set Up Websocket and Authorization Managers self.wsm = WebsocketManager(self.server) self.api_transports: Dict[str, APITransport] = { "websocket": self.wsm } mimetypes.add_type('text/plain', '.log') mimetypes.add_type('text/plain', '.gcode') mimetypes.add_type('text/plain', '.cfg') self.debug = config.getboolean('enable_debug_logging', False) log_level = logging.DEBUG if self.debug else logging.INFO logging.getLogger().setLevel(log_level) app_args: Dict[str, Any] = { 'serve_traceback': self.debug, 'websocket_ping_interval': 10, 'websocket_ping_timeout': 30, 'parent': self, 'default_handler_class': AuthorizedErrorHandler, 'default_handler_args': {}, 'log_function': self.log_request } # Set up HTTP only requests self.mutable_router = MutableRouter(self) app_handlers: List[Any] = [ (AnyMatches(), self.mutable_router), (r"/websocket", WebSocket), (r"/server/redirect", RedirectHandler)] self.app = tornado.web.Application(app_handlers, **app_args) self.get_handler_delegate = self.app.get_handler_delegate # Register handlers logfile = self.server.get_app_args().get('log_file') if logfile: self.register_static_file_handler( "moonraker.log", logfile, force=True) self.register_static_file_handler( "klippy.log", DEFAULT_KLIPPY_LOG_PATH, force=True)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.queued_jobs: Dict[str, QueuedJob] = {} self.lock = asyncio.Lock() self.load_on_start = config.getboolean("load_on_startup", False) self.automatic = config.getboolean("automatic_transition", False) self.queue_state: str = "ready" if self.automatic else "paused" self.job_delay = config.getfloat("job_transition_delay", 0.01) if self.job_delay <= 0.: raise config.error( "Value for option 'job_transition_delay' in section [job_queue]" " must be above 0.0") self.job_transition_gcode = config.get( "job_transition_gcode", "").strip() self.pop_queue_handle: Optional[asyncio.TimerHandle] = None self.server.register_event_handler( "server:klippy_ready", self._handle_ready) self.server.register_event_handler( "server:klippy_shutdown", self._handle_shutdown) self.server.register_event_handler( "job_state:complete", self._on_job_complete) self.server.register_event_handler( "job_state:error", self._on_job_abort) self.server.register_event_handler( "job_state:cancelled", self._on_job_abort) self.server.register_notification("job_queue:job_queue_changed") self.server.register_remote_method("pause_job_queue", self.pause_queue) self.server.register_remote_method("start_job_queue", self.start_queue) self.server.register_endpoint( "/server/job_queue/job", ['POST', 'DELETE'], self._handle_job_request) self.server.register_endpoint( "/server/job_queue/pause", ['POST'], self._handle_pause_queue) self.server.register_endpoint( "/server/job_queue/start", ['POST'], self._handle_start_queue) self.server.register_endpoint( "/server/job_queue/status", ['GET'], self._handle_queue_status)
def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper, app_params: Optional[Dict[str, Any]]) -> None: super().__init__(config, cmd_helper) self.config = config self.app_params = app_params self.debug = self.cmd_helper.is_debug_enabled() if app_params is not None: self.channel: str = app_params['channel'] self.path: pathlib.Path = pathlib.Path( app_params['path']).expanduser().resolve() executable: Optional[str] = app_params['executable'] self.type = CHANNEL_TO_TYPE[self.channel] else: self.type = config.get('type') self.channel = TYPE_TO_CHANNEL[self.type] self.path = pathlib.Path(config.get('path')).expanduser().resolve() executable = config.get('env', None) if self.channel not in CHANNEL_TO_TYPE.keys(): raise config.error(f"Invalid Channel '{self.channel}' for config " f"section [{config.get_name()}]") self._verify_path(config, 'path', self.path) self.executable: Optional[pathlib.Path] = None self.venv_args: Optional[str] = None if executable is not None: self.executable = pathlib.Path(executable).expanduser().resolve() self._verify_path(config, 'env', self.executable) self.venv_args = config.get('venv_args', None) self.is_service = config.getboolean("is_system_service", True) self.need_channel_update = False self._is_valid = False # We need to fetch all potential options for an Application. Not # all options apply to each subtype, however we can't limit the # options in children if we want to switch between channels and # satisfy the confighelper's requirements. self.origin: str = config.get('origin') self.primary_branch = config.get("primary_branch", "master") self.npm_pkg_json: Optional[pathlib.Path] = None if config.get("enable_node_updates", False): self.npm_pkg_json = self.path.joinpath("package-lock.json") self._verify_path(config, 'enable_node_updates', self.npm_pkg_json) self.python_reqs: Optional[pathlib.Path] = None if self.executable is not None: self.python_reqs = self.path.joinpath(config.get("requirements")) self._verify_path(config, 'requirements', self.python_reqs) self.install_script: Optional[pathlib.Path] = None install_script = config.get('install_script', None) if install_script is not None: self.install_script = self.path.joinpath(install_script).resolve() self._verify_path(config, 'install_script', self.install_script)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.ldap_host = config.get('ldap_host') self.ldap_port = config.getint("ldap_port", None) self.ldap_secure = config.getboolean("ldap_secure", False) base_dn_template = config.gettemplate('base_dn') self.base_dn = base_dn_template.render() self.group_dn: Optional[str] = None group_dn_template = config.gettemplate("group_dn", None) if group_dn_template is not None: self.group_dn = group_dn_template.render() self.active_directory = config.getboolean('is_active_directory', False) self.bind_dn: Optional[str] = None self.bind_password: Optional[str] = None bind_dn_template = config.gettemplate('bind_dn', None) bind_pass_template = config.gettemplate('bind_password', None) if bind_dn_template is not None: self.bind_dn = bind_dn_template.render() if bind_pass_template is None: raise config.error("Section [ldap]: Option 'bind_password' is " "required when 'bind_dn' is provided") self.bind_password = bind_pass_template.render() self.lock = asyncio.Lock()
def __init__(self, config: ConfigHelper, initial_val: Optional[int] = None) -> None: super().__init__(config) self.initial_state = config.getboolean('initial_state', False) self.timer: Optional[float] = config.getfloat('timer', None) if self.timer is not None and self.timer < 0.000001: raise config.error( f"Option 'timer' in section [{config.get_name()}] must " "be above 0.0") self.timer_handle: Optional[asyncio.TimerHandle] = None if initial_val is None: initial_val = int(self.initial_state) self.gpio_out = config.getgpioout('pin', initial_value=initial_val)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.debug_enabled = config.getboolean('enable_repo_debug', False) if self.debug_enabled: logging.warning("UPDATE MANAGER: REPO DEBUG ENABLED") shell_cmd: SCMDComp = self.server.lookup_component('shell_command') self.scmd_error = shell_cmd.error self.build_shell_command = shell_cmd.build_shell_command self.pkg_updater: Optional[PackageDeploy] = None AsyncHTTPClient.configure(None, defaults=dict(user_agent="Moonraker")) self.http_client = AsyncHTTPClient() self.github_request_cache: Dict[str, CachedGithubResponse] = {} # GitHub API Rate Limit Tracking self.gh_rate_limit: Optional[int] = None self.gh_limit_remaining: Optional[int] = None self.gh_limit_reset_time: Optional[float] = None # Update In Progress Tracking self.cur_update_app: Optional[str] = None self.cur_update_id: Optional[int] = None self.full_complete: bool = False
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.event_loop = self.server.get_event_loop() self.address: str = config.get('address') self.port: int = config.getint('port', 1883) user = config.gettemplate('username', None) self.user_name: Optional[str] = None if user: self.user_name = user.render() pw_file_path = config.get('password_file', None, deprecate=True) pw_template = config.gettemplate('password', None) self.password: Optional[str] = None if pw_file_path is not None: pw_file = pathlib.Path(pw_file_path).expanduser().absolute() if not pw_file.exists(): raise config.error( f"Password file '{pw_file}' does not exist") self.password = pw_file.read_text().strip() if pw_template is not None: self.password = pw_template.render() protocol = config.get('mqtt_protocol', "v3.1.1") self.protocol = MQTT_PROTOCOLS.get(protocol, None) if self.protocol is None: raise config.error( f"Invalid value '{protocol}' for option 'mqtt_protocol' " "in section [mqtt]. Must be one of " f"{MQTT_PROTOCOLS.values()}") self.instance_name = config.get('instance_name', socket.gethostname()) if '+' in self.instance_name or '#' in self.instance_name: raise config.error( "Option 'instance_name' in section [mqtt] cannot " "contain a wildcard.") self.qos = config.getint("default_qos", 0) if self.qos > 2 or self.qos < 0: raise config.error( "Option 'default_qos' in section [mqtt] must be " "between 0 and 2") self.client = paho_mqtt.Client(protocol=self.protocol) self.client.on_connect = self._on_connect self.client.on_message = self._on_message self.client.on_disconnect = self._on_disconnect self.client.on_publish = self._on_publish self.client.on_subscribe = self._on_subscribe self.client.on_unsubscribe = self._on_unsubscribe self.connect_evt: asyncio.Event = asyncio.Event() self.disconnect_evt: Optional[asyncio.Event] = None self.reconnect_task: Optional[asyncio.Task] = None self.subscribed_topics: SubscribedDict = {} self.pending_responses: List[asyncio.Future] = [] self.pending_acks: Dict[int, asyncio.Future] = {} self.server.register_endpoint( "/server/mqtt/publish", ["POST"], self._handle_publish_request, transports=["http", "websocket", "internal"]) self.server.register_endpoint( "/server/mqtt/subscribe", ["POST"], self._handle_subscription_request, transports=["http", "websocket", "internal"]) # Subscribe to API requests self.json_rpc = JsonRPC(transport="MQTT") self.api_request_topic = f"{self.instance_name}/moonraker/api/request" self.api_resp_topic = f"{self.instance_name}/moonraker/api/response" self.klipper_status_topic = f"{self.instance_name}/klipper/status" self.moonraker_status_topic = f"{self.instance_name}/moonraker/status" status_cfg: Dict[str, Any] = config.getdict("status_objects", {}, allow_empty_fields=True) self.status_objs: Dict[str, Any] = {} for key, val in status_cfg.items(): if val is not None: self.status_objs[key] = [v.strip() for v in val.split(',') if v.strip()] else: self.status_objs[key] = None if status_cfg: logging.debug(f"MQTT: Status Objects Set: {self.status_objs}") self.server.register_event_handler("server:klippy_identified", self._handle_klippy_identified) self.timestamp_deque: Deque = deque(maxlen=20) self.api_qos = config.getint('api_qos', self.qos) if config.getboolean("enable_moonraker_api", True): api_cache = self.server.register_api_transport("mqtt", self) for api_def in api_cache.values(): if "mqtt" in api_def.supported_transports: self.register_api_handler(api_def) self.subscribe_topic(self.api_request_topic, self._process_api_request, self.api_qos) self.server.register_remote_method("publish_mqtt_topic", self._publish_from_klipper) logging.info( f"\nReserved MQTT topics:\n" f"API Request: {self.api_request_topic}\n" f"API Response: {self.api_resp_topic}\n" f"Moonraker Status: {self.moonraker_status_topic}\n" f"Klipper Status: {self.klipper_status_topic}")
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.event_loop = self.server.get_event_loop() self.file_manager: FMComp = \ self.server.lookup_component('file_manager') self.klippy_apis: APIComp = \ self.server.lookup_component('klippy_apis') self.kinematics: str = "none" self.machine_name = config.get('machine_name', "Klipper") self.firmware_name: str = "Repetier | Klipper" self.last_message: Optional[str] = None self.last_gcode_response: Optional[str] = None self.current_file: str = "" self.file_metadata: Dict[str, Any] = {} self.enable_checksum = config.getboolean('enable_checksum', True) self.debug_queue: Deque[str] = deque(maxlen=100) # Initialize tracked state. self.printer_state: Dict[str, Dict[str, Any]] = { 'gcode_move': {}, 'toolhead': {}, 'virtual_sdcard': {}, 'fan': {}, 'display_status': {}, 'print_stats': {}, 'idle_timeout': {}, 'gcode_macro PANELDUE_BEEP': {} } self.extruder_count: int = 0 self.heaters: List[str] = [] self.is_ready: bool = False self.is_shutdown: bool = False self.initialized: bool = False self.cq_busy: bool = False self.gq_busy: bool = False self.command_queue: List[Tuple[FlexCallback, Any, Any]] = [] self.gc_queue: List[str] = [] self.last_printer_state: str = 'O' self.last_update_time: float = 0. # Set up macros self.confirmed_gcode: str = "" self.mbox_sequence: int = 0 self.available_macros: Dict[str, str] = {} self.confirmed_macros = { "RESTART": "RESTART", "FIRMWARE_RESTART": "FIRMWARE_RESTART" } macros = config.getlist('macros', None) if macros is not None: # The macro's configuration name is the key, whereas the full # command is the value self.available_macros = {m.split()[0]: m for m in macros} conf_macros = config.getlist('confirmed_macros', None) if conf_macros is not None: # The macro's configuration name is the key, whereas the full # command is the value self.confirmed_macros = {m.split()[0]: m for m in conf_macros} self.available_macros.update(self.confirmed_macros) self.non_trivial_keys = config.getlist('non_trivial_keys', ["Klipper state"]) self.ser_conn = SerialConnection(config, self) logging.info("PanelDue Configured") # Register server events self.server.register_event_handler("server:klippy_ready", self._process_klippy_ready) self.server.register_event_handler("server:klippy_shutdown", self._process_klippy_shutdown) self.server.register_event_handler("server:klippy_disconnect", self._process_klippy_disconnect) self.server.register_event_handler("server:status_update", self.handle_status_update) self.server.register_event_handler("server:gcode_response", self.handle_gcode_response) self.server.register_remote_method("paneldue_beep", self.paneldue_beep) # These commands are directly executued on the server and do not to # make a request to Klippy self.direct_gcodes: Dict[str, FlexCallback] = { 'M20': self._run_paneldue_M20, 'M30': self._run_paneldue_M30, 'M36': self._run_paneldue_M36, 'M408': self._run_paneldue_M408 } # These gcodes require special parsing or handling prior to being # sent via Klippy's "gcode/script" api command. self.special_gcodes: Dict[str, Callable[[List[str]], str]] = { 'M0': lambda args: "CANCEL_PRINT", 'M23': self._prepare_M23, 'M24': lambda args: "RESUME", 'M25': lambda args: "PAUSE", 'M32': self._prepare_M32, 'M98': self._prepare_M98, 'M120': lambda args: "SAVE_GCODE_STATE STATE=PANELDUE", 'M121': lambda args: "RESTORE_GCODE_STATE STATE=PANELDUE", 'M290': self._prepare_M290, 'M292': self._prepare_M292, 'M999': lambda args: "FIRMWARE_RESTART" }
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.address: str = config.get('address') self.port: int = config.getint('port', 1883) self.user_name = config.get('username', None) pw_file_path = config.get('password_file', None) self.password: Optional[str] = None if pw_file_path is not None: pw_file = pathlib.Path(pw_file_path).expanduser().absolute() if not pw_file.exists(): raise config.error(f"Password file '{pw_file}' does not exist") self.password = pw_file.read_text().strip() protocol = config.get('mqtt_protocol', "v3.1.1") self.protocol = MQTT_PROTOCOLS.get(protocol, None) if self.protocol is None: raise config.error( f"Invalid value '{protocol}' for option 'mqtt_protocol' " "in section [mqtt]. Must be one of " f"{MQTT_PROTOCOLS.values()}") self.instance_name = config.get('instance_name', socket.gethostname()) if '+' in self.instance_name or '#' in self.instance_name: raise config.error( "Option 'instance_name' in section [mqtt] cannot " "contain a wildcard.") self.qos = config.getint("default_qos", 0) if self.qos > 2 or self.qos < 0: raise config.error( "Option 'default_qos' in section [mqtt] must be " "between 0 and 2") self.client = paho_mqtt.Client(protocol=self.protocol) self.client.on_connect = self._on_connect self.client.on_message = self._on_message self.client.on_disconnect = self._on_disconnect self.client.on_publish = self._on_publish self.client.on_subscribe = self._on_subscribe self.client.on_unsubscribe = self._on_unsubscribe self.connect_evt: asyncio.Event = asyncio.Event() self.disconnect_evt: Optional[asyncio.Event] = None self.reconnect_task: Optional[asyncio.Task] = None self.subscribed_topics: SubscribedDict = {} self.pending_responses: List[asyncio.Future] = [] self.pending_acks: Dict[int, asyncio.Future] = {} self.server.register_endpoint("/server/mqtt/publish", ["POST"], self._handle_publish_request, transports=["http", "websocket"]) self.server.register_endpoint("/server/mqtt/subscribe", ["POST"], self._handle_subscription_request, transports=["http", "websocket"]) # Subscribe to API requests self.json_rpc = JsonRPC(transport="MQTT") self.api_request_topic = f"{self.instance_name}/moonraker/api/request" self.api_resp_topic = f"{self.instance_name}/moonraker/api/response" self.timestamp_deque: Deque = deque(maxlen=20) self.api_qos = config.getint('api_qos', self.qos) if config.getboolean("enable_moonraker_api", True): api_cache = self.server.register_api_transport("mqtt", self) for api_def in api_cache.values(): if "mqtt" in api_def.supported_transports: self.register_api_handler(api_def) self.subscribe_topic(self.api_request_topic, self._process_api_request, self.api_qos) logging.info( f"Moonraker API topics - Request: {self.api_request_topic}, " f"Response: {self.api_resp_topic}") IOLoop.current().spawn_callback(self._initialize)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.eventloop = self.server.get_event_loop() self.namespaces: Dict[str, object] = {} self.thread_lock = ThreadLock() self.database_path = os.path.expanduser( config.get('database_path', "~/.moonraker_database")) if not os.path.isdir(self.database_path): os.mkdir(self.database_path) self.lmdb_env = lmdb.open(self.database_path, map_size=MAX_DB_SIZE, max_dbs=MAX_NAMESPACES) with self.lmdb_env.begin(write=True, buffers=True) as txn: # lookup existing namespaces with txn.cursor() as cursor: remaining = cursor.first() while remaining: key = bytes(cursor.key()) self.namespaces[key.decode()] = self.lmdb_env.open_db( key, txn) remaining = cursor.next() if "moonraker" not in self.namespaces: mrdb = self.lmdb_env.open_db(b"moonraker", txn) self.namespaces["moonraker"] = mrdb txn.put(b'database_version', self._encode_value(DATABASE_VERSION), db=mrdb) # Iterate through all records, checking for invalid keys for ns, db in self.namespaces.items(): with txn.cursor(db=db) as cursor: remaining = cursor.first() while remaining: key_buf = cursor.key() try: decoded_key = bytes(key_buf).decode() except Exception: logging.info("Database Key Decode Error") decoded_key = '' if not decoded_key: hex_key = bytes(key_buf).hex() try: invalid_val = self._decode_value( cursor.value()) except Exception: invalid_val = "" logging.info( f"Invalid Key '{hex_key}' found in namespace " f"'{ns}', dropping value: {repr(invalid_val)}") try: remaining = cursor.delete() except Exception: logging.exception("Error Deleting LMDB Key") else: continue remaining = cursor.next() # Protected Namespaces have read-only API access. Write access can # be granted by enabling the debug option. Forbidden namespaces # have no API access. This cannot be overridden. self.protected_namespaces = set( self.get_item("moonraker", "database.protected_namespaces", ["moonraker"]).result()) self.forbidden_namespaces = set( self.get_item("moonraker", "database.forbidden_namespaces", []).result()) # Remove stale debug counter config.getboolean("enable_database_debug", False, deprecate=True) try: self.delete_item("moonraker", "database.debug_counter") except Exception: pass # Track unsafe shutdowns unsafe_shutdowns: int = self.get_item("moonraker", "database.unsafe_shutdowns", 0).result() msg = f"Unsafe Shutdown Count: {unsafe_shutdowns}" self.server.add_log_rollover_item("database", msg) # Increment unsafe shutdown counter. This will be reset if # moonraker is safely restarted self.insert_item("moonraker", "database.unsafe_shutdowns", unsafe_shutdowns + 1) self.server.register_endpoint("/server/database/list", ['GET'], self._handle_list_request) self.server.register_endpoint("/server/database/item", ["GET", "POST", "DELETE"], self._handle_item_request)
def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper) -> None: super().__init__(config, cmd_helper, prefix="Application") self.config = config self.debug = self.cmd_helper.is_debug_enabled() type_choices = list(TYPE_TO_CHANNEL.keys()) self.type = config.get('type').lower() if self.type not in type_choices: raise config.error( f"Config Error: Section [{config.get_name()}], Option " f"'type: {self.type}': value must be one " f"of the following choices: {type_choices}") self.channel = config.get("channel", TYPE_TO_CHANNEL[self.type]) if self.type == "zip_beta": self.server.add_warning( f"Config Section [{config.get_name()}], Option 'type: " "zip_beta', value 'zip_beta' is deprecated. Set 'type' " "to zip and 'channel' to 'beta'") self.type = "zip" self.path = pathlib.Path(config.get('path')).expanduser().resolve() executable = config.get('env', None) if self.channel not in SUPPORTED_CHANNELS[self.type]: raise config.error( f"Invalid Channel '{self.channel}' for config " f"section [{config.get_name()}], type: {self.type}") self._verify_path(config, 'path', self.path) self.executable: Optional[pathlib.Path] = None self.pip_exe: Optional[pathlib.Path] = None self.venv_args: Optional[str] = None if executable is not None: self.executable = pathlib.Path(executable).expanduser() self.pip_exe = self.executable.parent.joinpath("pip") if not self.pip_exe.exists(): self.server.add_warning( f"Update Manger {self.name}: Unable to locate pip " "executable") self._verify_path(config, 'env', self.executable) self.venv_args = config.get('venv_args', None) self.info_tags: List[str] = config.getlist("info_tags", []) self.managed_services: List[str] = [] svc_default = [] if config.getboolean("is_system_service", True): svc_default.append(self.name) svc_choices = [self.name, "klipper", "moonraker"] services: List[str] = config.getlist("managed_services", svc_default, separator=None) for svc in services: if svc not in svc_choices: raw = " ".join(services) self.server.add_warning( f"[{config.get_name()}]: Option 'restart_action: {raw}' " f"contains an invalid value '{svc}'. All values must be " f"one of the following choices: {svc_choices}") break for svc in svc_choices: if svc in services and svc not in self.managed_services: self.managed_services.append(svc) logging.debug( f"Extension {self.name} managed services: {self.managed_services}") # We need to fetch all potential options for an Application. Not # all options apply to each subtype, however we can't limit the # options in children if we want to switch between channels and # satisfy the confighelper's requirements. self.moved_origin: Optional[str] = config.get('moved_origin', None) self.origin: str = config.get('origin') self.primary_branch = config.get("primary_branch", "master") self.npm_pkg_json: Optional[pathlib.Path] = None if config.getboolean("enable_node_updates", False): self.npm_pkg_json = self.path.joinpath("package-lock.json") self._verify_path(config, 'enable_node_updates', self.npm_pkg_json) self.python_reqs: Optional[pathlib.Path] = None if self.executable is not None: self.python_reqs = self.path.joinpath(config.get("requirements")) self._verify_path(config, 'requirements', self.python_reqs) self.install_script: Optional[pathlib.Path] = None install_script = config.get('install_script', None) if install_script is not None: self.install_script = self.path.joinpath(install_script).resolve() self._verify_path(config, 'install_script', self.install_script)
def test_get_float_default(self, test_config: ConfigHelper): assert test_config.getboolean("invalid_option", None) is None
def test_get_float_fail(self, test_config: ConfigHelper): with pytest.raises(ConfigError): test_config.getboolean("invalid_option")
def test_get_boolean_exists(self, test_config: ConfigHelper): val = test_config.getboolean("test_bool") assert val is True
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.software_version = self.server.get_app_args().get( 'software_version') self.enable_ufp: bool = config.getboolean('enable_ufp', True) # Get webcam settings from config self.webcam: Dict[str, Any] = { 'flipH': config.getboolean('flip_h', False), 'flipV': config.getboolean('flip_v', False), 'rotate90': config.getboolean('rotate_90', False), 'streamUrl': config.get('stream_url', '/webcam/?action=stream'), 'webcamEnabled': config.getboolean('webcam_enabled', True), } # Local variables self.klippy_apis: APIComp = self.server.lookup_component('klippy_apis') self.heaters: Dict[str, Dict[str, Any]] = {} self.last_print_stats: Dict[str, Any] = {} # Register status update event self.server.register_event_handler('server:klippy_ready', self._init) self.server.register_event_handler('server:status_update', self._handle_status_update) # Version & Server information self.server.register_endpoint('/api/version', ['GET'], self._get_version, transports=['http'], wrap_result=False) self.server.register_endpoint('/api/server', ['GET'], self._get_server, transports=['http'], wrap_result=False) # Login, User & Settings self.server.register_endpoint('/api/login', ['POST'], self._post_login_user, transports=['http'], wrap_result=False) self.server.register_endpoint('/api/currentuser', ['GET'], self._post_login_user, transports=['http'], wrap_result=False) self.server.register_endpoint('/api/settings', ['GET'], self._get_settings, transports=['http'], wrap_result=False) # File operations # Note that file upload is handled in file_manager.py # TODO: List/info/select/delete files # Job operations self.server.register_endpoint('/api/job', ['GET'], self._get_job, transports=['http'], wrap_result=False) # TODO: start/cancel/restart/pause jobs # Printer operations self.server.register_endpoint('/api/printer', ['GET'], self._get_printer, transports=['http'], wrap_result=False) self.server.register_endpoint('/api/printer/command', ['POST'], self._post_command, transports=['http'], wrap_result=False) # TODO: head/tool/bed/chamber specific read/issue # Printer profiles self.server.register_endpoint('/api/printerprofiles', ['GET'], self._get_printerprofiles, transports=['http'], wrap_result=False) # Upload Handlers self.server.register_upload_handler( "/api/files/local", location_prefix="api/files/moonraker") self.server.register_endpoint( "/api/files/moonraker/(?P<relative_path>.+)", ['POST'], self._select_file, transports=['http'], wrap_result=False)
def __init__(self, config: ConfigHelper): super().__init__(config) self.initial_state = config.getboolean('initial_state', False)
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.login_timeout = config.getint('login_timeout', 90) self.force_logins = config.getboolean('force_logins', False) database: DBComp = self.server.lookup_component('database') database.register_local_namespace('authorized_users', forbidden=True) self.users = database.wrap_namespace('authorized_users') api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None) if api_user is None: self.api_key = uuid.uuid4().hex self.users[API_USER] = { 'username': API_USER, 'api_key': self.api_key, 'created_on': time.time() } else: self.api_key = api_user['api_key'] host_name, port = self.server.get_host_info() self.issuer = f"http://{host_name}:{port}" self.public_jwks: Dict[str, Dict[str, Any]] = {} for username, user_info in list(self.users.items()): if username == API_USER: continue if 'jwt_secret' in user_info: try: priv_key = self._load_private_key(user_info['jwt_secret']) jwk_id = user_info['jwk_id'] except (self.server.error, KeyError): logging.info("Invalid key found for user, removing") user_info.pop('jwt_secret', None) user_info.pop('jwk_id', None) self.users[username] = user_info continue self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key) self.trusted_users: Dict[IPAddr, Any] = {} self.oneshot_tokens: Dict[str, OneshotToken] = {} self.permitted_paths: Set[str] = set() # Get allowed cors domains self.cors_domains: List[str] = [] cors_cfg = config.get('cors_domains', "").strip() cds = [d.strip() for d in cors_cfg.split('\n') if d.strip()] for domain in cds: bad_match = re.search(r"^.+\.[^:]*\*", domain) if bad_match is not None: raise config.error( f"Unsafe CORS Domain '{domain}'. Wildcards are not" " permitted in the top level domain.") self.cors_domains.append( domain.replace(".", "\\.").replace("*", ".*")) # Get Trusted Clients self.trusted_ips: List[IPAddr] = [] self.trusted_ranges: List[IPNetwork] = [] self.trusted_domains: List[str] = [] tcs = config.get('trusted_clients', "") trusted_clients = [c.strip() for c in tcs.split('\n') if c.strip()] for val in trusted_clients: # Check IP address try: tc = ipaddress.ip_address(val) except ValueError: pass else: self.trusted_ips.append(tc) continue # Check ip network try: tc = ipaddress.ip_network(val) except ValueError: pass else: self.trusted_ranges.append(tc) continue # Check hostname self.trusted_domains.append(val.lower()) t_clients = "\n".join([str(ip) for ip in self.trusted_ips] + [str(rng) for rng in self.trusted_ranges] + self.trusted_domains) c_domains = "\n".join(self.cors_domains) logging.info(f"Authorization Configuration Loaded\n" f"Trusted Clients:\n{t_clients}\n" f"CORS Domains:\n{c_domains}") self.prune_handler = PeriodicCallback(self._prune_conn_handler, PRUNE_CHECK_TIME) self.prune_handler.start() # Register Authorization Endpoints self.permitted_paths.add("/server/redirect") self.permitted_paths.add("/access/login") self.permitted_paths.add("/access/refresh_jwt") self.server.register_endpoint("/access/login", ['POST'], self._handle_login, protocol=['http']) self.server.register_endpoint("/access/logout", ['POST'], self._handle_logout, protocol=['http']) self.server.register_endpoint("/access/refresh_jwt", ['POST'], self._handle_refresh_jwt, protocol=['http']) self.server.register_endpoint("/access/user", ['GET', 'POST', 'DELETE'], self._handle_user_request, protocol=['http']) self.server.register_endpoint("/access/users/list", ['GET'], self._handle_list_request, protocol=['http']) self.server.register_endpoint("/access/user/password", ['POST'], self._handle_password_reset, protocol=['http']) self.server.register_endpoint("/access/api_key", ['GET', 'POST'], self._handle_apikey_request, protocol=['http']) self.server.register_endpoint("/access/oneshot_token", ['GET'], self._handle_oneshot_request, protocol=['http']) self.server.register_notification("authorization:user_created") self.server.register_notification("authorization:user_deleted")
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.event_loop = self.server.get_event_loop() self.app_config = config.read_supplemental_config( SUPPLEMENTAL_CFG_PATH) auto_refresh_enabled = config.getboolean('enable_auto_refresh', False) self.channel = config.get('channel', "dev") if self.channel not in ["dev", "beta"]: raise config.error( f"Unsupported channel '{self.channel}' in section" " [update_manager]") self.cmd_helper = CommandHelper(config) self.updaters: Dict[str, BaseDeploy] = {} if config.getboolean('enable_system_updates', True): self.updaters['system'] = PackageDeploy(config, self.cmd_helper) if ( os.path.exists(KLIPPER_DEFAULT_PATH) and os.path.exists(KLIPPER_DEFAULT_EXEC) ): self.updaters['klipper'] = get_deploy_class(KLIPPER_DEFAULT_PATH)( self.app_config[f"update_manager klipper"], self.cmd_helper, { 'channel': self.channel, 'path': KLIPPER_DEFAULT_PATH, 'executable': KLIPPER_DEFAULT_EXEC }) else: self.updaters['klipper'] = BaseDeploy( self.app_config[f"update_manager klipper"], self.cmd_helper) self.updaters['moonraker'] = get_deploy_class(MOONRAKER_PATH)( self.app_config[f"update_manager moonraker"], self.cmd_helper, { 'channel': self.channel, 'path': MOONRAKER_PATH, 'executable': sys.executable }) # TODO: The below check may be removed when invalid config options # raise a config error. if ( config.get("client_repo", None) is not None or config.get('client_path', None) is not None ): raise config.error( "The deprecated 'client_repo' and 'client_path' options\n" "have been removed. See Moonraker's configuration docs\n" "for details on client configuration.") client_sections = config.get_prefix_sections("update_manager ") for section in client_sections: cfg = config[section] name = section.split()[-1] if name in self.updaters: raise config.error(f"Client repo {name} already added") client_type = cfg.get("type") if client_type in ["web", "web_beta"]: self.updaters[name] = WebClientDeploy(cfg, self.cmd_helper) elif client_type in ["git_repo", "zip", "zip_beta"]: path = os.path.expanduser(cfg.get('path')) self.updaters[name] = get_deploy_class(path)( cfg, self.cmd_helper) else: raise config.error( f"Invalid type '{client_type}' for section [{section}]") self.cmd_request_lock = asyncio.Lock() self.initialized_lock = asyncio.Event() self.klippy_identified_evt: Optional[asyncio.Event] = None # Auto Status Refresh self.last_refresh_time: float = 0 self.refresh_cb: Optional[PeriodicCallback] = None if auto_refresh_enabled: self.refresh_cb = PeriodicCallback( self._handle_auto_refresh, # type: ignore UPDATE_REFRESH_INTERVAL_MS) self.server.register_endpoint( "/machine/update/moonraker", ["POST"], self._handle_update_request) self.server.register_endpoint( "/machine/update/klipper", ["POST"], self._handle_update_request) self.server.register_endpoint( "/machine/update/system", ["POST"], self._handle_update_request) self.server.register_endpoint( "/machine/update/client", ["POST"], self._handle_update_request) self.server.register_endpoint( "/machine/update/full", ["POST"], self._handle_full_update_request) self.server.register_endpoint( "/machine/update/status", ["GET"], self._handle_status_request) self.server.register_endpoint( "/machine/update/recover", ["POST"], self._handle_repo_recovery) self.server.register_notification("update_manager:update_response") self.server.register_notification("update_manager:update_refreshed") # Register Ready Event self.server.register_event_handler( "server:klippy_identified", self._set_klipper_repo) # Initialize GitHub API Rate Limits and configured updaters self.event_loop.register_callback( self._initalize_updaters, list(self.updaters.values()))
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.login_timeout = config.getint('login_timeout', 90) self.force_logins = config.getboolean('force_logins', False) database: DBComp = self.server.lookup_component('database') database.register_local_namespace('authorized_users', forbidden=True) self.users = database.wrap_namespace('authorized_users') api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None) if api_user is None: self.api_key = uuid.uuid4().hex self.users[API_USER] = { 'username': API_USER, 'api_key': self.api_key, 'created_on': time.time() } else: self.api_key = api_user['api_key'] hi = self.server.get_host_info() self.issuer = f"http://{hi['hostname']}:{hi['port']}" self.public_jwks: Dict[str, Dict[str, Any]] = {} for username, user_info in list(self.users.items()): if username == API_USER: # Validate the API User for item in ["username", "api_key", "created_on"]: if item not in user_info: self.users[API_USER] = { 'username': API_USER, 'api_key': self.api_key, 'created_on': time.time() } break continue else: # validate created users valid = True for item in ["username", "password", "salt", "created_on"]: if item not in user_info: logging.info( f"Authorization: User {username} does not " f"contain field {item}, removing") del self.users[username] valid = False break if not valid: continue # generate jwks for valid users if 'jwt_secret' in user_info: try: priv_key = self._load_private_key(user_info['jwt_secret']) jwk_id = user_info['jwk_id'] except (self.server.error, KeyError): logging.info("Invalid key found for user, removing") user_info.pop('jwt_secret', None) user_info.pop('jwk_id', None) self.users[username] = user_info continue self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key) self.trusted_users: Dict[IPAddr, Any] = {} self.oneshot_tokens: Dict[str, OneshotToken] = {} self.permitted_paths: Set[str] = set() # Get allowed cors domains self.cors_domains: List[str] = [] for domain in config.getlist('cors_domains', []): bad_match = re.search(r"^.+\.[^:]*\*", domain) if bad_match is not None: raise config.error( f"Unsafe CORS Domain '{domain}'. Wildcards are not" " permitted in the top level domain.") if domain.endswith("/"): self.server.add_warning( f"[authorization]: Invalid domain '{domain}' in option " "'cors_domains'. Domain's cannot contain a trailing " "slash.") else: self.cors_domains.append( domain.replace(".", "\\.").replace("*", ".*")) # Get Trusted Clients self.trusted_ips: List[IPAddr] = [] self.trusted_ranges: List[IPNetwork] = [] self.trusted_domains: List[str] = [] for val in config.getlist('trusted_clients', []): # Check IP address try: tc = ipaddress.ip_address(val) except ValueError: pass else: self.trusted_ips.append(tc) continue # Check ip network try: tc = ipaddress.ip_network(val) except ValueError as e: if "has host bits set" in str(e): self.server.add_warning( f"[authorization]: Invalid CIDR expression '{val}' " "in option 'trusted_clients'") continue pass else: self.trusted_ranges.append(tc) continue # Check hostname match = re.match(r"([a-z0-9]+(-[a-z0-9]+)*\.?)+[a-z]{2,}$", val) if match is not None: self.trusted_domains.append(val.lower()) else: self.server.add_warning( f"[authorization]: Invalid domain name '{val}' " "in option 'trusted_clients'") t_clients = "\n".join([str(ip) for ip in self.trusted_ips] + [str(rng) for rng in self.trusted_ranges] + self.trusted_domains) c_domains = "\n".join(self.cors_domains) logging.info(f"Authorization Configuration Loaded\n" f"Trusted Clients:\n{t_clients}\n" f"CORS Domains:\n{c_domains}") eventloop = self.server.get_event_loop() self.prune_timer = eventloop.register_timer(self._prune_conn_handler) # Register Authorization Endpoints self.permitted_paths.add("/server/redirect") self.permitted_paths.add("/access/login") self.permitted_paths.add("/access/refresh_jwt") self.server.register_endpoint("/access/login", ['POST'], self._handle_login, transports=['http']) self.server.register_endpoint("/access/logout", ['POST'], self._handle_logout, transports=['http']) self.server.register_endpoint("/access/refresh_jwt", ['POST'], self._handle_refresh_jwt, transports=['http']) self.server.register_endpoint("/access/user", ['GET', 'POST', 'DELETE'], self._handle_user_request, transports=['http']) self.server.register_endpoint("/access/users/list", ['GET'], self._handle_list_request, transports=['http']) self.server.register_endpoint("/access/user/password", ['POST'], self._handle_password_reset, transports=['http']) self.server.register_endpoint("/access/api_key", ['GET', 'POST'], self._handle_apikey_request, transports=['http']) self.server.register_endpoint("/access/oneshot_token", ['GET'], self._handle_oneshot_request, transports=['http']) self.server.register_notification("authorization:user_created") self.server.register_notification("authorization:user_deleted")