def __init__(self, config: ConfigHelper) -> None: self.config = config 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.apprise = apprise.Apprise() self.warned = False self.attach_requires_file_system_check = True self.attach = config.get("attach", None) if self.attach is None or \ (self.attach.startswith("http://") or self.attach.startswith("https://")): self.attach_requires_file_system_check = False url_template = config.gettemplate('url') self.url = url_template.render() if len(self.url) < 2: raise config.error(f"Invalid url for: {config.get_name()}") self.title = config.gettemplate('title', None) self.body = config.gettemplate("body", None) self.events: List[str] = config.getlist("events", separator=",") self.apprise.add(self.url)
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, cmd_helper: CommandHelper, app_params: Optional[Dict[str, Any]]) -> None: super().__init__(config, cmd_helper, app_params) self.official_repo: str = "?" self.owner: str = "?" # Extract repo from origin for validation match = re.match(r"https?://(?:www\.)?github.com/([^/]+/[^.]+)", self.origin) if match is not None: self.official_repo = match.group(1) self.owner = self.official_repo.split('/')[0] else: raise config.error( "Invalid url set for 'origin' option in section " f"[{config.get_name()}]. Unable to extract owner/repo.") self.host_repo: str = config.get('host_repo', self.official_repo) self.detected_type: str = "?" self.source_checksum: str = "" self.pristine = False self.verified = False self.build_date: int = 0 self.full_version: str = "?" self.short_version: str = "?" self.commit_hash: str = "?" self.lastest_hash: str = "?" self.latest_version: str = "?" self.latest_checksum: str = "?" self.latest_build_date: int = 0 self.commit_log: List[Dict[str, Any]] = [] self.package_list: List[str] = [] self.python_pkg_list: List[str] = [] self.release_download_info: Tuple[str, str, int] = ("?", "?", 0) self.errors: List[str] = [] self.mutex: asyncio.Lock = asyncio.Lock() self.refresh_event: Optional[asyncio.Event] = None
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() dist_info: Dict[str, Any] dist_info = {'name': distro.name(pretty=True)} dist_info.update(distro.info()) dist_info['release_info'] = distro.distro_release_info() self.inside_container = False self.system_info: Dict[str, Any] = { 'python': { "version": sys.version_info, "version_string": sys.version.replace("\n", " ") }, 'cpu_info': self._get_cpu_info(), 'sd_info': self._get_sdcard_info(), 'distribution': dist_info, 'virtualization': self._check_inside_container() } self._update_log_rollover(log=True) providers: Dict[str, type] = { "none": BaseProvider, "systemd_cli": SystemdCliProvider, "systemd_dbus": SystemdDbusProvider } ptype = config.get('provider', 'systemd_dbus') pclass = providers.get(ptype) if pclass is None: raise config.error(f"Invalid Provider: {ptype}") self.sys_provider: BaseProvider = pclass(config) logging.info(f"Using System Provider: {ptype}") self.server.register_endpoint("/machine/reboot", ['POST'], self._handle_machine_request) self.server.register_endpoint("/machine/shutdown", ['POST'], self._handle_machine_request) self.server.register_endpoint("/machine/services/restart", ['POST'], self._handle_service_request) self.server.register_endpoint("/machine/services/stop", ['POST'], self._handle_service_request) self.server.register_endpoint("/machine/services/start", ['POST'], self._handle_service_request) self.server.register_endpoint("/machine/system_info", ['GET'], self._handle_sysinfo_request) self.server.register_notification("machine:service_state_changed") # Register remote methods self.server.register_remote_method("shutdown_machine", self.sys_provider.shutdown) self.server.register_remote_method("reboot_machine", self.sys_provider.reboot) # IP network shell commands shell_cmd: SCMDComp = self.server.load_component( config, 'shell_command') self.addr_cmd = shell_cmd.build_shell_command("ip -json address") iwgetbin = "/sbin/iwgetid" if not pathlib.Path(iwgetbin).exists(): iwgetbin = "iwgetid" self.iwgetid_cmd = shell_cmd.build_shell_command(iwgetbin) self.init_evt = asyncio.Event()
def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper ) -> None: super().__init__(config, cmd_helper) self.repo = config.get('repo').strip().strip("/") self.owner = self.repo.split("/", 1)[0] self.path = pathlib.Path(config.get("path")).expanduser().resolve() self.type = config.get('type') self.channel = "stable" if self.type == "web" else "beta" self.persistent_files: List[str] = [] pfiles = config.get('persistent_files', None) if pfiles is not None: self.persistent_files = [pf.strip().strip("/") for pf in pfiles.split("\n") if pf.strip()] if ".version" in self.persistent_files: raise config.error( "Invalid value for option 'persistent_files': " "'.version' can not be persistent") self.version: str = "?" self.remote_version: str = "?" self.dl_info: Tuple[str, str, int] = ("?", "?", 0) self.refresh_evt: Optional[asyncio.Event] = None self.mutex: asyncio.Lock = asyncio.Lock() logging.info(f"\nInitializing Client Updater: '{self.name}'," f"\nChannel: {self.channel}" f"\npath: {self.path}")
def __init__(self: WLED, config: ConfigHelper) -> None: # root_logger = logging.getLogger() # root_logger.setLevel(logging.DEBUG) self.server = config.get_server() prefix_sections = config.get_prefix_sections("wled") logging.info(f"WLED component loading strips: {prefix_sections}") strip_types = {"HTTP": StripHttp, "SERIAL": StripSerial} self.strips = {} for section in prefix_sections: cfg = config[section] try: name_parts = cfg.get_name().split(maxsplit=1) if len(name_parts) != 2: raise cfg.error(f"Invalid Section Name: {cfg.get_name()}") name: str = name_parts[1] logging.info(f"WLED strip: {name}") # Discard old color_order setting, always support 4 color strips _ = cfg.get("color_order", "", deprecate=True) strip_type: str = cfg.get("type", "http") strip_class: Optional[Type[Strip]] strip_class = strip_types.get(strip_type.upper()) if strip_class is None: raise config.error(f"Unsupported Strip Type: {strip_type}") self.strips[name] = strip_class(name, cfg) except Exception as e: # Ensures errors such as "Color not supported" are visible msg = f"Failed to initialise strip [{cfg.get_name()}]\n{e}" self.server.add_warning(msg) continue # Register two remote methods for GCODE self.server.register_remote_method("set_wled_state", self.set_wled_state) self.server.register_remote_method("set_wled", self.set_wled) # As moonraker is about making things a web api, let's try it # Yes, this is largely a cut-n-paste from power.py self.server.register_endpoint("/machine/wled/strips", ["GET"], self._handle_list_strips) self.server.register_endpoint("/machine/wled/status", ["GET"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/on", ["POST"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/off", ["POST"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/toggle", ["POST"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/strip", ["GET", "POST"], self._handle_single_wled_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() if not HAS_GPIOD: self.server.add_warning("Unable to load gpiod library, GPIO power " "devices will not be loaded") self.chip_factory = GpioChipFactory() self.devices: Dict[str, PowerDevice] = {} prefix_sections = config.get_prefix_sections("power") logging.info(f"Power component loading devices: {prefix_sections}") dev_types = { "gpio": GpioDevice, "tplink_smartplug": TPLinkSmartPlug, "tasmota": Tasmota, "shelly": Shelly, "homeseer": HomeSeer, "homeassistant": HomeAssistant, "loxonev1": Loxonev1 } try: for section in prefix_sections: cfg = config[section] dev_type: str = cfg.get("type") dev_class: Optional[Type[PowerDevice]] dev_class = dev_types.get(dev_type) if dev_class is None: raise config.error(f"Unsupported Device Type: {dev_type}") dev = dev_class(cfg) if isinstance(dev, GpioDevice): if not HAS_GPIOD: continue dev.configure_line(cfg, self.chip_factory) self.devices[dev.get_name()] = dev except Exception: self.chip_factory.close() raise self.server.register_endpoint("/machine/device_power/devices", ['GET'], self._handle_list_devices) self.server.register_endpoint("/machine/device_power/status", ['GET'], self._handle_batch_power_request) self.server.register_endpoint("/machine/device_power/on", ['POST'], self._handle_batch_power_request) self.server.register_endpoint("/machine/device_power/off", ['POST'], self._handle_batch_power_request) self.server.register_endpoint("/machine/device_power/device", ['GET', 'POST'], self._handle_single_power_request) self.server.register_remote_method("set_device_power", self.set_device_power) self.server.register_event_handler("server:klippy_shutdown", self._handle_klippy_shutdown) self.server.register_notification("power:power_changed") event_loop = self.server.get_event_loop() event_loop.register_callback(self._initalize_devices, list(self.devices.values()))
def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.devices: Dict[str, PowerDevice] = {} prefix_sections = config.get_prefix_sections("power") logging.info(f"Power component loading devices: {prefix_sections}") dev_types = { "gpio": GpioDevice, "tplink_smartplug": TPLinkSmartPlug, "tasmota": Tasmota, "shelly": Shelly, "homeseer": HomeSeer, "homeassistant": HomeAssistant, "loxonev1": Loxonev1, "rf": RFDevice, "mqtt": MQTTDevice } for section in prefix_sections: cfg = config[section] dev_type: str = cfg.get("type") dev_class: Optional[Type[PowerDevice]] dev_class = dev_types.get(dev_type) if dev_class is None: raise config.error(f"Unsupported Device Type: {dev_type}") try: dev = dev_class(cfg) except Exception as e: msg = f"Failed to load power device [{cfg.get_name()}]\n{e}" self.server.add_warning(msg) continue self.devices[dev.get_name()] = dev self.server.register_endpoint( "/machine/device_power/devices", ['GET'], self._handle_list_devices) self.server.register_endpoint( "/machine/device_power/status", ['GET'], self._handle_batch_power_request) self.server.register_endpoint( "/machine/device_power/on", ['POST'], self._handle_batch_power_request) self.server.register_endpoint( "/machine/device_power/off", ['POST'], self._handle_batch_power_request) self.server.register_endpoint( "/machine/device_power/device", ['GET', 'POST'], self._handle_single_power_request) self.server.register_remote_method( "set_device_power", self.set_device_power) self.server.register_event_handler( "server:klippy_shutdown", self._handle_klippy_shutdown) self.server.register_event_handler( "file_manager:upload_queued", self._handle_upload_queued) self.server.register_notification("power:power_changed")
def __init__(self, config: ConfigHelper) -> None: super().__init__(config) if self.off_when_shutdown: raise config.error( "Option 'off_when_shutdown' in section " f"[{config.get_name()}] is unsupported for 'klipper_device'") if self.klipper_restart: raise config.error( "Option 'restart_klipper_when_powered' in section " f"[{config.get_name()}] is unsupported for 'klipper_device'") if (self.bound_service is not None and self.bound_service.startswith("klipper")): # Klipper devices cannot be bound to an instance of klipper or # klipper_mcu raise config.error( f"Option 'bound_service' cannot be set to {self.bound_service}" f" for 'klipper_device' [{config.get_name()}]") self.is_shutdown: bool = False self.update_fut: Optional[asyncio.Future] = None self.request_mutex = asyncio.Lock() self.timer: Optional[float] = config.getfloat('timer', None, above=0.000001) self.timer_handle: Optional[asyncio.TimerHandle] = None self.object_name = config.get('object_name') obj_parts = self.object_name.split() self.gc_cmd = f"SET_PIN PIN={obj_parts[-1]} " if obj_parts[0] == "gcode_macro": self.gc_cmd = obj_parts[-1] elif obj_parts[0] != "output_pin": raise config.error( "Klipper object must be either 'output_pin' or 'gcode_macro' " f"for option 'object_name' in section [{config.get_name()}]") self.server.register_event_handler("server:status_update", self._status_update) self.server.register_event_handler("server:klippy_ready", self._handle_ready) self.server.register_event_handler("server:klippy_disconnect", self._handle_disconnect)
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, 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: WLED, config: ConfigHelper) -> None: try: # root_logger = logging.getLogger() # root_logger.setLevel(logging.DEBUG) self.server = config.get_server() prefix_sections = config.get_prefix_sections("wled") logging.info(f"WLED component loading strips: {prefix_sections}") color_orders = {"RGB": ColorOrder.RGB, "RGBW": ColorOrder.RGBW} self.strips = {} for section in prefix_sections: cfg = config[section] name_parts = cfg.get_name().split(maxsplit=1) if len(name_parts) != 2: raise cfg.error(f"Invalid Section Name: {cfg.get_name()}") name: str = name_parts[1] logging.info(f"WLED strip: {name}") color_order_cfg: str = cfg.get("color_order", "RGB") color_order = color_orders.get(color_order_cfg) if color_order is None: raise config.error( f"Color order not supported: {color_order_cfg}") self.strips[name] = Strip(name, color_order, cfg) # Register two remote methods for GCODE self.server.register_remote_method("set_wled_state", self.set_wled_state) self.server.register_remote_method("set_wled", self.set_wled) # As moonraker is about making things a web api, let's try it # Yes, this is largely a cut-n-paste from power.py self.server.register_endpoint("/machine/wled/strips", ["GET"], self._handle_list_strips) self.server.register_endpoint("/machine/wled/status", ["GET"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/on", ["POST"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/off", ["POST"], self._handle_batch_wled_request) self.server.register_endpoint("/machine/wled/strip", ["GET", "POST"], self._handle_single_wled_request) except Exception as e: logging.exception(e)
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 configure_line(self, config: ConfigHelper, chip_factory: GpioChipFactory) -> None: pin, chip_id, invert = self._parse_pin(config) try: chip = chip_factory.get_gpio_chip(chip_id) self.line = chip.get_line(pin) if invert: self.line.request(consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT, flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW) else: self.line.request(consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT) except Exception: self.state = "error" logging.exception( f"Unable to init {pin}. Make sure the gpio is not in " "use by another program or exported by sysfs.") raise config.error("Power GPIO Config Error")
def __init__(self, config: ConfigHelper, cmd_helper: CommandHelper) -> None: super().__init__(config, cmd_helper) self.need_channel_update = self.type != "zip" self.official_repo: str = "?" self.owner: str = "?" # Extract repo from origin for validation match = re.match(r"https?://(?:www\.)?github.com/([^/]+/[^.]+)", self.origin) if match is not None: self.official_repo = match.group(1) self.owner = self.official_repo.split('/')[0] else: raise config.error( "Invalid url set for 'origin' option in section " f"[{config.get_name()}]. Unable to extract owner/repo.") self.host_repo: str = config.get('host_repo', self.official_repo) self.package_list: List[str] = [] self.python_pkg_list: List[str] = [] self.release_download_info: Tuple[str, str, int] = ("?", "?", 0)
def _parse_pin(self, config: ConfigHelper) -> Tuple[int, str, bool]: pin = cfg_pin = config.get("pin") invert = False if pin[0] == "!": pin = pin[1:] invert = True chip_id: str = "gpiochip0" pin_parts = pin.split("/") if len(pin_parts) == 2: chip_id, pin = pin_parts elif len(pin_parts) == 1: pin = pin_parts[0] # Verify pin if not chip_id.startswith("gpiochip") or \ not chip_id[-1].isdigit() or \ not pin.startswith("gpio") or \ not pin[4:].isdigit(): raise config.error(f"Invalid Power Pin configuration: {cfg_pin}") pin_id = int(pin[4:]) return pin_id, chip_id, invert
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) -> 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) -> None: self.server = config.get_server() self.eventloop = self.server.get_event_loop() self.name = config.get_name().split()[-1] self.itransport: ITransport = self.server.lookup_component( 'internal_transport') self.mutex = asyncio.Lock() gpio: GpioFactory = self.server.load_component(config, 'gpio') self.gpio_event = gpio.register_gpio_event(config.get('pin'), self._on_gpio_event) min_event_time = config.getfloat('minimum_event_time', .05, minval=.010) self.gpio_event.setup_debounce(min_event_time, self._on_gpio_error) self.press_template = config.gettemplate("on_press", None, is_async=True) self.release_template = config.gettemplate("on_release", None, is_async=True) if (self.press_template is None and self.release_template is None): raise config.error( f"[{config.get_name()}]: No template option configured") self.notification_sent: bool = False self.user_data: Dict[str, Any] = {} self.context: Dict[str, Any] = { 'call_method': self.itransport.call_method, 'send_notification': self._send_notification, 'event': { 'elapsed_time': 0., 'received_time': 0., 'render_time': 0., 'pressed': False, }, 'user_data': self.user_data }
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 _verify_path(self, config: ConfigHelper, option: str, file_path: pathlib.Path) -> None: if not file_path.exists(): raise config.error( f"Invalid path for option `{option}` in section " f"[{config.get_name()}]: Path `{file_path}` does not exist")
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.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")
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.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, 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)