UNPKG

iobroker.octoprint

Version:
891 lines (773 loc) 69.6 kB
'use strict'; const utils = require('@iobroker/adapter-core'); const https = require('node:https'); const axios = require('axios').default; const adapterName = require('./package.json').name.split('.').pop(); const pluginDisplayLayerProgress = require('./lib/plugins/displaylayerprogress'); const pluginSlicerThumbnails = require('./lib/plugins/slicerthumbnails'); class OctoPrint extends utils.Adapter { constructor(options) { super({ ...options, name: adapterName, useFormatDate: true, }); this.supportedVersion = '1.10.3'; this.displayedVersionWarning = false; this.apiConnected = false; this.systemCommands = []; this.printerStatus = 'API not connected'; this.printerOperational = false; this.printerPrinting = false; this.refreshStateTimeout = null; this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('unload', this.onUnload.bind(this)); } async onReady() { this.setApiConnected(false); if (!this.config.octoprintIp) { this.log.warn(`OctoPrint ip / hostname not configured - check instance configuration`); return; } if (!this.config.octoprintApiKey) { this.log.warn(`API key not configured - check instance configuration`); return; } if (this.config.customName) { this.setStateChangedAsync('name', { val: this.config.customName, ack: true }); } else { this.setStateChangedAsync('name', { val: '', ack: true }); } await this.subscribeStatesAsync('*'); // Delete old (unused) namespace on startup await this.delObjectAsync('printjob.progress.printtime_left'); await this.delObjectAsync('temperature', { recursive: true }); this.refreshState('onReady'); } /** * @param {string} id * @param {ioBroker.State | null | undefined} state */ onStateChange(id, state) { // No ack = changed by user if (id && state && !state.ack) { const idNoNamespace = this.removeNamespace(id); if (this.apiConnected) { if (idNoNamespace.match(new RegExp('tools.tool[0-9]{1}.(targetTemperature|extrude)'))) { const matches = idNoNamespace.match(/tools\.(tool[0-9]{1})\.(targetTemperature|extrude)$/); const toolId = matches[1]; const command = matches[2]; if (command === 'targetTemperature') { this.log.debug(`changing target "${toolId}" temperature to ${state.val}`); const targetObj = {}; targetObj[toolId] = state.val; // https://docs.octoprint.org/en/master/api/printer.html#issue-a-tool-command this.buildServiceRequest('printer/tool', { command: 'target', targets: targetObj, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – If targets or offsets contains a property or tool contains a value not matching the format tool{n}, the target/offset temperature, extrusion amount or flow rate factor is not a valid number or outside of the supported range, or if the request is otherwise invalid. // 409 Conflict – If the printer is not operational or – in case of select or extrude – currently printing. this.log.error(`(printer/tool) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/tool) error: ${err}`); }); } else if (command === 'extrude') { this.log.debug(`extruding ${state.val}mm`); // https://docs.octoprint.org/en/master/api/printer.html#issue-a-tool-command this.buildServiceRequest('printer/tool', { command: 'extrude', amount: state.val, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – If targets or offsets contains a property or tool contains a value not matching the format tool{n}, the target/offset temperature, extrusion amount or flow rate factor is not a valid number or outside of the supported range, or if the request is otherwise invalid. // 409 Conflict – If the printer is not operational or – in case of select or extrude – currently printing. this.log.error(`(printer/tool) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/tool) error: ${err}`); }); } } else if (idNoNamespace === 'tools.bed.targetTemperature') { this.log.debug(`changing target bed temperature to ${state.val}°C`); // https://docs.octoprint.org/en/master/api/printer.html#issue-a-bed-command this.buildServiceRequest('printer/bed', { command: 'target', target: state.val, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – If target or offset is not a valid number or outside of the supported range, or if the request is otherwise invalid. // 409 Conflict – If the printer is not operational or the selected printer profile does not have a heated bed. this.log.error(`(printer/bed) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/bed) error: ${err}`); }); } else if (idNoNamespace === 'command.printer') { const allowedCommandsConnection = ['connect', 'disconnect', 'fake_ack']; const allowedCommandsPrinter = ['home']; if (allowedCommandsConnection.indexOf(state.val) > -1) { this.log.debug(`sending printer connection command: ${state.val}`); // https://docs.octoprint.org/en/master/api/connection.html#issue-a-connection-command this.buildServiceRequest('connection', { command: state.val, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); this.refreshState('onStateChange command.printer'); } else { // 400 Bad Request – If the selected port or baudrate for a connect command are not part of the available options. this.log.error(`(connection) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(connection) error: ${err}`); }); } else if (allowedCommandsPrinter.indexOf(state.val) > -1) { this.log.debug(`sending printer command: ${state.val}`); // https://docs.octoprint.org/en/master/api/printer.html#issue-a-print-head-command this.buildServiceRequest('printer/printhead', { command: state.val, axes: ['x', 'y', 'z'], }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – Invalid axis specified, invalid value for travel amount for a jog command or factor for feed rate or otherwise invalid request. // 409 Conflict – If the printer is not operational or currently printing. this.log.error(`(printer/printhead) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/printhead) error: ${err}`); }); } else { this.log.error('printer command not allowed: ' + state.val + '. Choose one of: ' + allowedCommandsConnection.concat(allowedCommandsPrinter).join(', ')); } } else if (idNoNamespace === 'command.printjob') { const allowedCommands = ['start', 'pause', 'resume', 'cancel', 'restart']; if (allowedCommands.indexOf(state.val) > -1) { this.log.debug(`sending printjob command: ${state.val}`); const printjobCommand = { command: state.val, }; // Pause command needs an action if (state.val === 'pause') { printjobCommand.action = 'pause'; } // Workaround: Resume is a pause command with resume action if (state.val === 'resume') { printjobCommand.command = 'pause'; printjobCommand.action = 'resume'; } // https://docs.octoprint.org/en/master/api/job.html#issue-a-job-command this.buildServiceRequest('job', printjobCommand) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 409 Conflict – If the printer is not operational or the current print job state does not match the preconditions for the command. this.log.error(`(job) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(job) error: ${err}`); }); } else { this.log.error('print job command not allowed: ' + state.val + '. Choose one of: ' + allowedCommands.join(', ')); } } else if (idNoNamespace === 'command.sd') { const allowedCommands = ['init', 'refresh', 'release']; if (allowedCommands.indexOf(state.val) > -1) { this.log.debug(`sending sd card command: ${state.val}`); // https://docs.octoprint.org/en/master/api/printer.html#issue-an-sd-command this.buildServiceRequest('printer/sd', { command: state.val, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 409 Conflict – If a refresh or release command is issued but the SD card has not been initialized (e.g. via init). this.log.error(`(printer/sd) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/sd) error: ${err}`); }); } else { this.log.error('sd card command not allowed: ' + state.val + '. Choose one of: ' + allowedCommands.join(', ')); } } else if (idNoNamespace === 'command.custom') { this.log.debug(`sending custom command: ${state.val}`); // https://docs.octoprint.org/en/master/api/printer.html#send-an-arbitrary-command-to-the-printer this.buildServiceRequest('printer/command', { command: state.val, }) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 409 Conflict – If the printer is not operational this.log.error(`(printer/command) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/command) error: ${err}`); }); } else if (idNoNamespace === 'command.system') { if (this.systemCommands.indexOf(state.val) > -1) { this.log.debug(`sending system command: ${state.val}`); // https://docs.octoprint.org/en/master/api/system.html#execute-a-registered-system-command this.buildServiceRequest('system/commands/' + state.val, {}) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – If a divider is supposed to be executed or if the request is malformed otherwise // 404 Not Found – If the command could not be found for source and action // 500 Internal Server Error – If the command didn’t define a command to execute, the command returned a non-zero return code and ignore was not true or some other internal server error occurred this.log.error(`(system/commands/*) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/commands/*) error: ${err}`); }); } else { this.log.error(`system command not allowed: ${state.val}. Choose one of: ${this.systemCommands.join(', ')}`); } } else if (idNoNamespace.indexOf('command.jog.') === 0) { // Validate jog value if (state.val !== 0) { const axis = id.split('.').pop(); // Last element of the object id is the axis const jogCommand = { command: 'jog', }; // Add axis jogCommand[axis] = state.val; this.log.debug(`sending jog ${axis} command: ${state.val}`); // https://docs.octoprint.org/en/master/api/printer.html#issue-a-print-head-command this.buildServiceRequest('printer/printhead', jogCommand) .then((response) => { if (response.status === 204) { this.setStateAsync(idNoNamespace, { val: state.val, ack: true }); } else { // 400 Bad Request – Invalid axis specified, invalid value for travel amount for a jog command or factor for feed rate or otherwise invalid request. // 409 Conflict – If the printer is not operational or currently printing. this.log.error(`(printer/printhead) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(printer/printhead) error: ${err}`); }); } else { this.log.error('Jog: provide non-zero jog value'); } } else if (idNoNamespace.match(new RegExp('files.[a-zA-Z0-9_]+.(select|print)'))) { const matches = idNoNamespace.match(/files\.([a-zA-Z0-9_]+)\.(select|print)$/); const fileId = matches[1]; const action = matches[2]; this.log.debug(`selecting/printing file "${fileId}" - action: "${action}"`); this.getState(`files.${fileId}.path`, (err, state) => { const fullPath = state?.val; this.log.debug(`selecting/printing file with path "${fullPath}"`); // https://docs.octoprint.org/en/master/api/files.html#issue-a-file-command this.buildServiceRequest(`files/${fullPath}`, { command: 'select', print: action === 'print', }) .then((response) => { if (response.status === 204) { this.log.debug('selecting/printing file successful'); this.refreshState(`onStateChange file.${action}`); } else { this.log.error(`(files/*) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(files/*) error: ${err}`); }); }); } } } } setApiConnected(connection) { this.setStateChangedAsync('info.connection', { val: connection, ack: true }); this.apiConnected = connection; if (!connection) { this.log.debug('API is offline'); this.printerStatus = 'API not connected'; this.setStateChangedAsync('printer_status', { val: this.printerStatus, ack: true }); } } async refreshState(source) { this.log.debug(`refreshState: started from "${source}"`); // https://docs.octoprint.org/en/master/api/version.html this.buildServiceRequest('version', null) .then((response) => { if (response.status === 200) { this.setApiConnected(true); this.log.debug(`connected to OctoPrint API - online! - status: ${response.status}`); this.setStateChangedAsync('meta.version', { val: response.data.server, ack: true }); this.setStateChangedAsync('meta.api_version', { val: response.data.api, ack: true }); if (this.isNewerVersion(response.data.server, this.supportedVersion) && !this.displayedVersionWarning) { this.log.warn( `You should update your OctoPrint installation - supported version of this adapter is ${this.supportedVersion} (or later). Your current version is ${response.data.server}`, ); this.displayedVersionWarning = true; // Just show once } this.refreshStateDetails(); } else { this.log.error(`(version) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((error) => { this.log.debug(`(version) received error - API is now offline: ${JSON.stringify(error)}`); this.setApiConnected(false); }); // Delete old timer if (this.refreshStateTimeout) { this.log.debug(`refreshStateTimeout: CLEARED by ${source}`); this.clearTimeout(this.refreshStateTimeout); } // Start a new timeout in any case if (!this.apiConnected) { const notConnectedTimeout = 10; this.refreshStateTimeout = this.setTimeout(() => { this.refreshStateTimeout = null; this.refreshState('timeout (API not connected)'); }, notConnectedTimeout * 1000); this.log.debug(`refreshStateTimeout: re-created refresh timeout (API not connected): id ${this.refreshStateTimeout} - seconds: ${notConnectedTimeout}`); } else if (this.printerPrinting) { this.refreshStateTimeout = this.setTimeout(() => { this.refreshStateTimeout = null; this.refreshState('timeout (printing)'); }, this.config.apiRefreshIntervalPrinting * 1000); // Default 10 sec this.log.debug(`refreshStateTimeout: re-created refresh timeout (printing): id ${this.refreshStateTimeout} - seconds: ${this.config.apiRefreshIntervalPrinting}`); } else if (this.printerOperational) { this.refreshStateTimeout = this.setTimeout(() => { this.refreshStateTimeout = null; this.refreshState('timeout (operational)'); }, this.config.apiRefreshIntervalOperational * 1000); // Default 30 sec this.log.debug(`refreshStateTimeout: re-created refresh timeout (operational): id ${this.refreshStateTimeout} - seconds: ${this.config.apiRefreshIntervalOperational}`); } else { this.refreshStateTimeout = this.setTimeout(() => { this.refreshStateTimeout = null; this.refreshState('timeout (default)'); }, this.config.apiRefreshInterval * 1000); // Default 60 sec this.log.debug(`refreshStateTimeout: re-created refresh timeout (default): id ${this.refreshStateTimeout} - seconds: ${this.config.apiRefreshInterval}`); } } async refreshStateDetails() { if (this.apiConnected) { // https://docs.octoprint.org/en/master/api/connection.html this.buildServiceRequest('connection', null) .then((response) => { if (response.status === 200) { this.updatePrinterStatus(response.data.current.state); if (!this.printerPrinting) { this.refreshFiles(); } // Try again in 2 seconds if (this.printerStatus === 'Detecting serial connection') { this.setTimeout(() => { this.refreshState('detecting serial connection'); }, 2000); } } else { this.log.error(`(connection) status ${response.status}: ${JSON.stringify(response.data)}`); } }) .catch((err) => { this.log.debug(`(connection) error: ${err}`); }); if (this.printerOperational) { this.buildServiceRequest('printer', null) .then(async (response) => { const content = response.data; if (content?.temperature) { for (const key of Object.keys(content.temperature)) { const obj = content.temperature[key]; const isTool = key.indexOf('tool') > -1; const isBed = key == 'bed'; if (isTool || isBed) { // Tool + bed information // Create tool channel await this.setObjectNotExistsAsync(`tools.${key}`, { type: 'channel', common: { name: key, }, native: {}, }); // Set actual temperature await this.setObjectNotExistsAsync(`tools.${key}.actualTemperature`, { type: 'state', common: { name: { en: 'Actual temperature', de: 'Tatsächliche Temperatur', ru: 'Фактическая температура', pt: 'Temperatura real', nl: 'Werkelijke temperatuur', fr: 'Température réelle', it: 'Temperatura effettiva', es: 'Temperatura real', pl: 'Rzeczywista temperatura', uk: 'Погода', 'zh-cn': '实际温度', }, type: 'number', role: 'value.temperature', unit: '°C', read: true, write: false, def: 0, }, native: {}, }); await this.setStateChangedAsync(`tools.${key}.actualTemperature`, { val: obj.actual, ack: true }); // Set target temperature await this.setObjectNotExistsAsync(`tools.${key}.targetTemperature`, { type: 'state', common: { name: { en: 'Target temperature', de: 'Zieltemperatur', ru: 'Целевая температура', pt: 'Temperatura alvo', nl: 'Doeltemperatuur', fr: 'Température cible', it: 'Temperatura obiettivo', es: 'Temperatura objetivo', pl: 'Temperatura docelowa', uk: 'Цільова температура', 'zh-cn': '目标温度', }, type: 'number', role: 'value.temperature', unit: '°C', read: true, write: true, }, native: {}, }); await this.setStateChangedAsync(`tools.${key}.targetTemperature`, { val: obj.target, ack: true }); // Set offset temperature await this.setObjectNotExistsAsync(`tools.${key}.offsetTemperature`, { type: 'state', common: { name: { en: 'Offset temperature', de: 'Offset-Temperatur', ru: 'Смещение температуры', pt: 'Temperatura compensada', nl: 'Offset temperatuur', fr: 'Température de décalage', it: 'Temperatura di compensazione', es: 'Temperatura de compensación', pl: 'Temperatura przesunięcia', uk: 'Температура офсету', 'zh-cn': '偏移温度', }, type: 'number', role: 'value.temperature', unit: '°C', read: true, write: false, def: 0, }, native: {}, }); await this.setStateChangedAsync(`tools.${key}.offsetTemperature`, { val: obj.target, ack: true }); } if (isTool) { // Set extrude await this.setObjectNotExistsAsync(`tools.${key}.extrude`, { type: 'state', common: { name: { en: 'Extrude', de: 'Extrudieren', ru: 'Выдавливание', pt: 'Extrudar', nl: 'extruderen', fr: 'Extruder', it: 'Estrudere', es: 'Extrudir', pl: 'Wyrzucać', uk: 'Екструдед', 'zh-cn': '拉伸', }, type: 'number', role: 'value', unit: 'mm', read: true, write: true, def: 0, }, native: {}, }); } } } }) .catch((err) => { this.log.debug(`(printer) error: ${err}`); }); } else { // https://docs.octoprint.org/en/master/api/system.html#list-all-registered-system-commands this.buildServiceRequest('system/commands', null) .then((response) => { if (response.status === 200) { this.systemCommands = []; for (const key of Object.keys(response.data)) { const arr = response.data[key]; arr.forEach((e) => this.systemCommands.push(`${e.source}/${e.action}`)); } this.log.debug(`(system/commands) registered commands: ${this.systemCommands.join(', ')}`); } }) .catch((err) => { this.log.debug(`(system/commands) error: ${err}`); }); } // Plugin Display Layer Progress // https://github.com/OllisGit/OctoPrint-DisplayLayerProgress if (this.config.pluginDisplayLayerProgress) { this.log.debug('[plugin display layer progress] plugin activated - fetching details'); pluginDisplayLayerProgress.refreshValues(this); } else { await this.delObjectAsync('plugins.displayLayerProgress', { recursive: true }); } if (this.printerOperational || this.printerPrinting) { // https://docs.octoprint.org/en/master/api/job.html#retrieve-information-about-the-current-job this.buildServiceRequest('job', null) .then(async (response) => { if (response.status === 200) { const content = response.data; if (content?.error) { this.log.warn(`print job error: ${content.error}`); } if (content?.job?.file) { const filePath = `${content.job.file.origin}/${content.job.file.path}`; if (this.config.pluginSlicerThumbnails) { await this.setObjectNotExistsAsync('printjob.file.thumbnail_url', { type: 'state', common: { name: { en: 'Thumbnail URL', de: 'Miniaturbild-URL', ru: 'URL миниатюры', pt: 'URL da miniatura', nl: 'Miniatuur-URL', fr: 'URL de la miniature', it: 'URL miniatura', es: 'URL de la miniatura', pl: 'URL miniatury', uk: 'Веб-сайт', 'zh-cn': '缩略图网址', }, type: 'string', role: 'url', read: true, write: false, }, native: {}, }); this.log.debug(`[plugin slicer thumbnails] trying to find current print job thumbnail url`); const fileObjectsView = await this.getObjectViewAsync('system', 'channel', { startkey: this.namespace + '.files.', endkey: this.namespace + '.files.\u9999', }); let foundThumbnail = false; if (fileObjectsView && fileObjectsView.rows) { // File file where native.path matches current jobs file path const currentFileObject = fileObjectsView.rows.find((fileObj) => fileObj.value?.native?.path === filePath); if (currentFileObject) { const currentFileId = this.removeNamespace(currentFileObject.id); try { this.log.debug(`[plugin slicer thumbnails] found current file: ${currentFileId}`); const currentFileThumbnailUrlState = await this.getStateAsync(`${currentFileId}.thumbnail.url`); if (currentFileThumbnailUrlState && currentFileThumbnailUrlState.val) { foundThumbnail = true; await this.setStateChangedAsync('printjob.file.thumbnail_url', { val: currentFileThumbnailUrlState.val, ack: true }); } } catch { this.log.debug(`[plugin slicer thumbnails] unable to get value of state ${currentFileId}.thumbnail.url`); } } } if (!foundThumbnail) { this.log.debug(`[plugin slicer thumbnails] unable to find file which matches current job file`); await this.setStateChangedAsync('printjob.file.thumbnail_url', { val: null, ack: true }); } } else { await this.delObjectAsync('printjob.file.thumbnail_url'); } await this.setStateChangedAsync('printjob.file.name', { val: content.job.file.name, ack: true }); await this.setStateChangedAsync('printjob.file.origin', { val: content.job.file.origin, ack: true }); await this.setStateChangedAsync('printjob.file.size', { val: Number((content.job.file.size / 1024).toFixed(2)), ack: true }); await this.setStateChangedAsync('printjob.file.date', { val: new Date(content.job.file.date * 1000).getTime(), ack: true }); if (content?.job?.filament) { let filamentLength = 0; let filamentVolume = 0; if (content.job.filament?.tool0) { filamentLength = content.job.filament?.tool0?.length ?? 0; filamentVolume = content.job.filament?.tool0?.volume ?? 0; } else { filamentLength = content.job.filament?.length ?? 0; filamentVolume = content.job.filament?.volume ?? 0; } if (typeof filamentLength == 'number' && typeof filamentVolume == 'number') { await this.setStateChangedAsync('printjob.filament.length', { val: Number((filamentLength / 1000).toFixed(2)), ack: true }); await this.setStateChangedAsync('printjob.filament.volume', { val: Number(filamentVolume.toFixed(2)), ack: true }); } else { this.log.debug('Filament length and/or volume contains no valid number'); await this.setStateChangedAsync('printjob.filament.length', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.filament.volume', { val: 0, ack: true }); } } else { await this.setStateChangedAsync('printjob.filament.length', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.filament.volume', { val: 0, ack: true }); } } if (content?.progress) { await this.setStateChangedAsync('printjob.progress.completion', { val: Math.round(content.progress.completion), ack: true }); await this.setStateChangedAsync('printjob.progress.filepos', { val: Number((content.progress.filepos / 1024).toFixed(2)), ack: true }); await this.setStateChangedAsync('printjob.progress.printtime', { val: content.progress.printTime, ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeLeft', { val: content.progress.printTimeLeft, ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeFormat', { val: this.printtimeString(content.progress.printTime), ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeLeftFormat', { val: this.printtimeString(content.progress.printTimeLeft), ack: true }); const finishedAt = new Date(); finishedAt.setSeconds(finishedAt.getSeconds() + content.progress.printTimeLeft); await this.setStateChangedAsync('printjob.progress.finishedAt', { val: finishedAt.getTime(), ack: true }); await this.setStateChangedAsync('printjob.progress.finishedAtFormat', { val: this.formatDate(finishedAt), ack: true }); } } }) .catch((err) => { this.log.debug(`(job) error: ${err}`); }); } else { this.log.debug('refreshing job state: skipped detail refresh (not printing)'); // Reset all values await this.setStateChangedAsync('printjob.file.name', { val: '', ack: true }); await this.setStateChangedAsync('printjob.file.origin', { val: '', ack: true }); await this.setStateChangedAsync('printjob.file.size', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.file.date', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.filament.length', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.filament.volume', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.progress.completion', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.progress.filepos', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.progress.printtime', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeLeft', { val: 0, ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeFormat', { val: this.printtimeString(0), ack: true }); await this.setStateChangedAsync('printjob.progress.printtimeLeftFormat', { val: this.printtimeString(0), ack: true }); } } else { this.log.debug('refreshing state: skipped detail refresh (API not connected)'); } } flattenFiles(files) { let fileArr = []; if (Array.isArray(files)) { for (const file of files) { if (file.type == 'machinecode' && file.origin == 'local') { const fileObj = { name: file.display, path: file.origin + '/' + file.path, date: file.date ? new Date(file.date * 1000).getTime() : 0, size: file.size ? Number(Math.round(file.size / 1024).toFixed(2)) : 0, thumbnail: null, }; // Plugin Slicer Thumbnails if (this.config.pluginSlicerThumbnails) { if (file?.thumbnail_src === 'prusaslicerthumbnails') { fileObj.thumbnail = file.thumbnail; } } fileArr.push(fileObj); } else if (file.type == 'folder') { fileArr = fileArr.concat(this.flattenFiles(file.children)); } } } return fileArr; } async refreshFiles() { if (this.apiConnected) { this.log.debug('[refreshFiles] started'); const filesAll = []; const filesKeep = []; try { const fileChannels = await this.getChannelsOfAsync('files'); // Collect all existing files if (fileChannels) { for (let i = 0; i < fileChannels.length; i++) { const idNoNamespace = this.removeNamespace(fileChannels[i]._id); // Check if the state is a direct child (e.g. files.MyCustomFile) if (idNoNamespace.split('.').length === 2) { if (!fileChannels[i].native.path) { // Force recreation of files without native path (upgraded from older version) await this.delObjectAsync(idNoNamespace, { recursive: true }); this.log.debug(`[refreshFiles] found file channel without native.path - deleted ${idNoNamespace}`); } else { filesAll.push(idNoNamespace); } } } } } catch (err) { this.log.warn(err); } this.buildServiceRequest('files?recursive=true', null) .then(async (response) => { if (response.status === 200) { const content = response.data; const fileList = this.flattenFiles(content.files); this.log.debug(`[refreshFiles] found ${fileList.length} files`); for (const f in fileList) { const file = fileList[f]; const fileNameClean = this.cleanNamespace(file.path.replace('.gcode', '').replace('/', ' ')); this.log.debug(`[refreshFiles] found file "${fileNameClean}" (clean name) - location: ${file.path}`); filesKeep.push(`files.${fileNameClean}`); await this.setObjectNotExistsAsync(`files.${fileNameClean}`, { type: 'channel', common: { name: file.name, }, native: { path: file.path, }, }); await this.setObjectNotExistsAsync(`files.${fileNameClean}.name`, { type: 'state', common: { name: { en: 'File name', de: 'Dateiname', ru: 'Имя файла', pt: 'Nome do arquivo', nl: 'Bestandsnaam', fr: 'Nom de fichier', it: 'Nome del file', es: 'Nombre del archivo',