UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

271 lines (270 loc) 9.62 kB
import "../printer-api.interface.js"; import { ExternalServiceError } from "../../exceptions/runtime.exceptions.js"; import { httpToWsUrl } from "../../utils/url.utils.js"; import { normalizeUrl } from "../../utils/normalize-url.js"; import { AppConstants } from "../../server.constants.js"; import { SOCKET_STATE } from "../../shared/dtos/socket-state.type.js"; import { API_STATE } from "../../shared/dtos/api-state.type.js"; import { HttpStatusCode } from "../../constants/http-status-codes.constants.js"; import { WebsocketAdapter } from "../../shared/websocket.adapter.js"; //#region src/services/octoprint/octoprint-websocket.adapter.ts const WsMessage = { WS_OPENED: "WS_OPENED", WS_CLOSED: "WS_CLOSED", WS_ERROR: "WS_ERROR", API_STATE_UPDATED: "API_STATE_UPDATED", WS_STATE_UPDATED: "WS_STATE_UPDATED" }; const OctoPrintMessage = { connected: "connected", reauthRequired: "reauthRequired", current: "current", history: "history", event: "event", plugin: "plugin", timelapse: "timelapse", slicingProgress: "slicingProgress" }; const octoPrintEvent = (event) => `octoprint.${event}`; var OctoprintWebsocketAdapter = class OctoprintWebsocketAdapter extends WebsocketAdapter { printerType = 0; printerId; stateUpdated = false; stateUpdateTimestamp = null; socketState = SOCKET_STATE.unopened; apiStateUpdated = false; apiStateUpdateTimestamp = null; apiState = API_STATE.unset; lastMessageReceivedTimestamp = null; reauthRequired = false; reauthRequiredTimestamp = null; login; logger; socketURL; sessionDto; username; refreshPrinterCurrentInterval; constructor(loggerFactory, octoprintClient, eventEmitter2, configService) { super(loggerFactory); this.octoprintClient = octoprintClient; this.eventEmitter2 = eventEmitter2; this.configService = configService; this.logger = loggerFactory(OctoprintWebsocketAdapter.name); } get _debugMode() { return this.configService.get(AppConstants.debugSocketStatesKey, AppConstants.defaultDebugSocketStates) === "true"; } needsReopen() { return this.apiState === API_STATE.responding && (this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.error); } needsSetup() { return this.socketState === SOCKET_STATE.unopened; } needsReauth() { return this.reauthRequired; } isClosedOrAborted() { return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted; } registerCredentials(socketLogin) { const { printerId, loginDto } = socketLogin; this.printerId = printerId; this.login = loginDto; const httpUrlString = normalizeUrl(this.login.printerURL); const httpUrlPath = new URL(httpUrlString).pathname; const wsUrl = httpToWsUrl(httpUrlString); wsUrl.pathname = (httpUrlPath ?? "/") + "sockjs/websocket"; this.socketURL = wsUrl; } open() { if (this.socket) throw new Error(`Socket already exists by printerId, ignoring open request`); super.open(this.socketURL); } close() { clearInterval(this.refreshPrinterCurrentInterval); super.close(); } async sendThrottle(throttle = AppConstants.defaultSocketThrottleRate) { return await this.sendMessage(JSON.stringify({ throttle })); } async reauthSession() { this.logger.log("Sending reauthSession"); await this.setupSocketSession(); await this.sendAuth(); this.resetReauthRequired(); } /** * Retrieve session token by authenticating with OctoPrint API */ async setupSocketSession() { this.resetSocketState(); this.sessionDto = await this.octoprintClient.login(this.login).then((d) => { const r = d.data; if (r.name === "_api") { this.setApiState("globalKey"); this.setSocketState("aborted"); throw new ExternalServiceError("Global API Key detected, aborting socket connection", "OctoPrint"); } else if (r.needs?.group[0] === "guests") { this.logger.warn("Detected group guests in OctoPrint login response, marking as unauthorized"); this.setApiState("authFail"); this.setSocketState("aborted"); throw new ExternalServiceError("Guest group detected, authentication failed, aborting socket connection", "OctoPrint"); } this.setApiState("responding"); this.setSocketState("opening"); return r; }).catch((e) => { this.setSocketState("aborted"); if (e instanceof ExternalServiceError) { this.logger.warn(`Printer authorization error, apiState: ${this.apiState}`); throw e; } else { if (e?.response?.status === 403) { this.setApiState("authFail"); this.setSocketState("aborted"); throw new ExternalServiceError(e, "OctoPrint"); } this.logger.error(`Printer (${this.printerId}) network or transport error, marking it as unreachable; ${e}`); this.setApiState("noResponse"); } throw e; }); this.username = await this.octoprintClient.getAdminUserOrDefault(this.login).catch((e) => { const status = e.response?.status; this.setApiState("authFail"); this.setSocketState("aborted"); if (status && [ HttpStatusCode.BAD_GATEWAY, HttpStatusCode.NOT_IMPLEMENTED, HttpStatusCode.SERVICE_UNAVAILABLE, HttpStatusCode.GATEWAY_TIMEOUT ].includes(status)) this.logger.error(`Detected a 501-504 error (${status}) probably OctoPrint has crashed or is restarting`); throw e; }); await this.updateCurrentStateSafely(); this.logger.log(`Setting up printer current interval loop with 10 seconds interval`); if (this.refreshPrinterCurrentInterval) clearInterval(this.refreshPrinterCurrentInterval); this.refreshPrinterCurrentInterval = setInterval(async () => { await this.updateCurrentStateSafely(); }, 1e4); } setReauthRequired() { this.reauthRequired = true; this.reauthRequiredTimestamp = Date.now(); } resetReauthRequired() { this.reauthRequired = false; this.reauthRequiredTimestamp = null; } resetSocketState() { this.setSocketState("unopened"); this.setApiState("unset"); } emitEventSync(event, payload) { if (!this.eventEmittingAllowed) return; this.eventEmitter2.emit(octoPrintEvent(event), { event, payload, printerId: this.printerId, printerType: 0 }); } async afterOpened(_) { this.setSocketState("opened"); await this.sendAuth(); await this.sendThrottle(AppConstants.defaultSocketThrottleRate); } async onMessage(message) { this.lastMessageReceivedTimestamp = Date.now(); if (this.socketState !== SOCKET_STATE.authenticated) this.setSocketState("authenticated"); const data = JSON.parse(message); const eventName = Object.keys(data)[0]; const payload = data[eventName]; if (this._debugMode) this.logger.log(`RX Msg ${eventName} ${message.substring(0, 140)}...`); if (eventName === OctoPrintMessage.reauthRequired) { this.logger.log("Received 'reauthRequired', acting on it"); this.setReauthRequired(); } await this.emitEvent(eventName, payload); } async afterClosed(event) { this.setSocketState("closed"); delete this.socket; await this.emitEvent(WsMessage.WS_CLOSED, "connection closed"); } async onError(error) { this.setSocketState("error"); await this.emitEvent(WsMessage.WS_ERROR, error?.length ? error : "connection error"); } /** * Re-fetch the printer current state without depending on Websocket * @private */ async updateCurrentStateSafely() { try { const current = await this.octoprintClient.getPrinterCurrent(this.login, true); const isOperational = current.data?.state?.flags?.operational; let job = {}; if (isOperational) job = (await this.octoprintClient.getJob(this.login)).data; this.setApiState(API_STATE.responding); return await this.emitEvent("current", { ...current.data, progress: job?.progress, job: job?.job }); } catch (e) { if (e.isAxiosError) { const castError = e; if (castError?.response?.status == 409) { this.logger.error(`Printer current interval loop error`); await this.emitEvent("current", { state: { flags: { operational: false, error: false }, text: "USB disconnected", error: castError?.response.data.error } }); return; } this.logger.error(`Could not update Octoprint current due to a request error`); this.setApiState(API_STATE.noResponse); return; } this.logger.error(`Could not update Octoprint current due to an unknown error`); this.setApiState(API_STATE.noResponse); } } async emitEvent(event, payload) { if (!this.eventEmittingAllowed) return; await this.eventEmitter2.emitAsync(octoPrintEvent(event), { event, payload, printerId: this.printerId, printerType: 0 }); } async sendAuth() { if (!this.sessionDto?.session?.length) throw new Error("Cant send auth, session is unset."); this.setSocketState(SOCKET_STATE.authenticating); await this.sendMessage(JSON.stringify({ auth: `${this.username}:${this.sessionDto.session}` })); } setSocketState(state) { this.socketState = state; this.stateUpdated = true; this.stateUpdateTimestamp = Date.now(); if (this._debugMode) this.logger.log(`${this.printerId} Socket state updated to: ` + state); this.emitEventSync(WsMessage.WS_STATE_UPDATED, state); } setApiState(state) { if (state === API_STATE.globalKey) this.logger.warn("Global API Key WS State detected"); this.apiState = state; this.apiStateUpdated = true; this.apiStateUpdateTimestamp = Date.now(); if (this._debugMode) this.logger.log(`${this.printerId} API state updated to: ` + state); this.emitEventSync(WsMessage.API_STATE_UPDATED, state); } }; //#endregion export { OctoPrintMessage, OctoprintWebsocketAdapter, WsMessage, octoPrintEvent }; //# sourceMappingURL=octoprint-websocket.adapter.js.map