iobroker.octoprint
Version:
ioBroker OctoPrint Adapter
891 lines (773 loc) • 69.6 kB
JavaScript
'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',