First Commit
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Stream Deck files
|
||||||
|
*.sdPlugin/bin
|
||||||
|
*.sdPlugin/logs
|
||||||
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Attach to Plugin",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"processId": "${command:PickProcess}",
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/bin/**/*.js"
|
||||||
|
],
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
/* JSON schemas */
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"**/manifest.json"
|
||||||
|
],
|
||||||
|
"url": "https://schemas.elgato.com/streamdeck/plugins/manifest.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"**/layouts/*.json"
|
||||||
|
],
|
||||||
|
"url": "https://schemas.elgato.com/streamdeck/plugins/layout.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 265 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 827 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
111
com.carsten-graf.octocontroll.sdPlugin/manifest.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"Name": "OctoControll",
|
||||||
|
"Version": "0.1.0.0",
|
||||||
|
"Author": "Carsten Graf",
|
||||||
|
"Actions": [
|
||||||
|
{
|
||||||
|
"Name": "Printer Status",
|
||||||
|
"UUID": "com.octoprint.monitor.printer-status",
|
||||||
|
"Icon": "imgs/actions/counter/icon",
|
||||||
|
"Tooltip": "Shows printer and job status from OctoPrint.",
|
||||||
|
"PropertyInspectorPath": "ui/increment-counter.html",
|
||||||
|
"Controllers": [
|
||||||
|
"Keypad"
|
||||||
|
],
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "imgs/actions/counter/key",
|
||||||
|
"TitleAlignment": "middle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Pause / Resume Print",
|
||||||
|
"UUID": "com.octoprint.monitor.print-pause",
|
||||||
|
"Icon": "imgs/actions/counter/icon",
|
||||||
|
"Tooltip": "Pause or resume the current OctoPrint job.",
|
||||||
|
"PropertyInspectorPath": "ui/increment-counter.html",
|
||||||
|
"Controllers": [
|
||||||
|
"Keypad"
|
||||||
|
],
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "imgs/actions/counter/key",
|
||||||
|
"TitleAlignment": "middle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Cancel Print",
|
||||||
|
"UUID": "com.octoprint.monitor.print-cancel",
|
||||||
|
"Icon": "imgs/actions/counter/icon",
|
||||||
|
"Tooltip": "Cancel the current OctoPrint job (with confirmation).",
|
||||||
|
"PropertyInspectorPath": "ui/increment-counter.html",
|
||||||
|
"Controllers": [
|
||||||
|
"Keypad"
|
||||||
|
],
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "imgs/actions/counter/key",
|
||||||
|
"TitleAlignment": "middle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Temperatures",
|
||||||
|
"UUID": "com.octoprint.monitor.temperature",
|
||||||
|
"Icon": "imgs/actions/counter/icon",
|
||||||
|
"Tooltip": "Show hotend and bed temperatures from OctoPrint.",
|
||||||
|
"PropertyInspectorPath": "ui/increment-counter.html",
|
||||||
|
"Controllers": [
|
||||||
|
"Keypad"
|
||||||
|
],
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "imgs/actions/counter/key",
|
||||||
|
"TitleAlignment": "middle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Home Axes",
|
||||||
|
"UUID": "com.octoprint.monitor.home-axes",
|
||||||
|
"Icon": "imgs/actions/counter/icon",
|
||||||
|
"Tooltip": "Home all axes on the printer via OctoPrint.",
|
||||||
|
"PropertyInspectorPath": "ui/increment-counter.html",
|
||||||
|
"Controllers": [
|
||||||
|
"Keypad"
|
||||||
|
],
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "imgs/actions/counter/key",
|
||||||
|
"TitleAlignment": "middle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "OctoControll",
|
||||||
|
"CategoryIcon": "imgs/plugin/category-icon",
|
||||||
|
"CodePath": "bin/plugin.js",
|
||||||
|
"Description": "Controll your Octoprint instance",
|
||||||
|
"Icon": "imgs/plugin/marketplace",
|
||||||
|
"SDKVersion": 3,
|
||||||
|
"Software": {
|
||||||
|
"MinimumVersion": "6.9"
|
||||||
|
},
|
||||||
|
"OS": [
|
||||||
|
{
|
||||||
|
"Platform": "mac",
|
||||||
|
"MinimumVersion": "12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Platform": "windows",
|
||||||
|
"MinimumVersion": "10"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Nodejs": {
|
||||||
|
"Version": "20",
|
||||||
|
"Debug": "enabled"
|
||||||
|
},
|
||||||
|
"UUID": "com.carsten-graf.octocontroll"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head lang="en">
|
||||||
|
<title>OctoPrint Connection</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<script src="https://sdpi-components.dev/releases/v4/sdpi-components.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<sdpi-item label="Host">
|
||||||
|
<sdpi-textfield setting="host" placeholder="http://octopi.local"></sdpi-textfield>
|
||||||
|
</sdpi-item>
|
||||||
|
|
||||||
|
<sdpi-item label="Port">
|
||||||
|
<sdpi-textfield setting="port" placeholder="80"></sdpi-textfield>
|
||||||
|
</sdpi-item>
|
||||||
|
|
||||||
|
<sdpi-item label="API Key">
|
||||||
|
<sdpi-textfield setting="apiKey" placeholder="Paste your OctoPrint API key"></sdpi-textfield>
|
||||||
|
</sdpi-item>
|
||||||
|
|
||||||
|
<sdpi-item label="Show Temp in Status">
|
||||||
|
<sdpi-checkbox setting="showTemperature">Show temperatures on Printer Status key</sdpi-checkbox>
|
||||||
|
</sdpi-item>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3112
package-lock.json
generated
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"watch": "rollup -c -w --watch.onEnd=\"streamdeck restart com.carsten-graf.octocontroll\""
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@elgato/cli": "^1.7.1",
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.2",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.0",
|
||||||
|
"@tsconfig/node20": "^20.1.2",
|
||||||
|
"@types/node": "~20.15.0",
|
||||||
|
"rollup": "^4.0.2",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@elgato/streamdeck": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
rollup.config.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||||
|
import terser from "@rollup/plugin-terser";
|
||||||
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
import path from "node:path";
|
||||||
|
import url from "node:url";
|
||||||
|
|
||||||
|
const isWatching = !!process.env.ROLLUP_WATCH;
|
||||||
|
const sdPlugin = "com.carsten-graf.octocontroll.sdPlugin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('rollup').RollupOptions}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
input: "src/plugin.ts",
|
||||||
|
output: {
|
||||||
|
file: `${sdPlugin}/bin/plugin.js`,
|
||||||
|
sourcemap: isWatching,
|
||||||
|
sourcemapPathTransform: (relativeSourcePath, sourcemapPath) => {
|
||||||
|
return url.pathToFileURL(path.resolve(path.dirname(sourcemapPath), relativeSourcePath)).href;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "watch-externals",
|
||||||
|
buildStart: function () {
|
||||||
|
this.addWatchFile(`${sdPlugin}/manifest.json`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typescript({
|
||||||
|
mapRoot: isWatching ? "./" : undefined
|
||||||
|
}),
|
||||||
|
nodeResolve({
|
||||||
|
browser: false,
|
||||||
|
exportConditions: ["node"],
|
||||||
|
preferBuiltins: true
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
!isWatching && terser(),
|
||||||
|
{
|
||||||
|
name: "emit-module-package-file",
|
||||||
|
generateBundle() {
|
||||||
|
this.emitFile({ fileName: "package.json", source: `{ "type": "module" }`, type: "asset" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
43
src/actions/home-axes.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// src/actions/home-axes.ts
|
||||||
|
import streamDeck, { action, KeyDownEvent, SingletonAction } from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { OctoPrintClient, OctoPrintSettings } from "../octoprint-client";
|
||||||
|
|
||||||
|
@action({ UUID: "com.octoprint.monitor.home-axes" })
|
||||||
|
export class HomeAxesAction extends SingletonAction {
|
||||||
|
override async onKeyDown(ev: KeyDownEvent): Promise<void> {
|
||||||
|
const settings = ev.payload.settings as unknown as OctoPrintSettings;
|
||||||
|
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await ev.action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const printer = await client.getPrinterState();
|
||||||
|
|
||||||
|
if (printer.state.flags.printing) {
|
||||||
|
await ev.action.setTitle("Printing!\nBlocked");
|
||||||
|
setTimeout(() => ev.action.setTitle("Home"), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!printer.state.flags.operational) {
|
||||||
|
await ev.action.setTitle("Offline");
|
||||||
|
setTimeout(() => ev.action.setTitle("Home"), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ev.action.setTitle("Homing…");
|
||||||
|
await client.homeAxes(["x", "y", "z"]);
|
||||||
|
await ev.action.setTitle("✓ Homed");
|
||||||
|
setTimeout(() => ev.action.setTitle("Home"), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
streamDeck.logger.error("HomeAxes: failed", err);
|
||||||
|
await ev.action.setTitle("⚠️ Error");
|
||||||
|
setTimeout(() => ev.action.setTitle("Home"), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/actions/print-cancel.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// src/actions/print-cancel.ts
|
||||||
|
import streamDeck, { action, KeyDownEvent, SingletonAction } from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { OctoPrintClient, OctoPrintSettings } from "../octoprint-client";
|
||||||
|
|
||||||
|
@action({ UUID: "com.octoprint.monitor.print-cancel" })
|
||||||
|
export class PrintCancelAction extends SingletonAction {
|
||||||
|
// Track confirmation state per action instance
|
||||||
|
private confirmPending = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
override async onKeyDown(ev: KeyDownEvent): Promise<void> {
|
||||||
|
const settings = ev.payload.settings as unknown as OctoPrintSettings;
|
||||||
|
const actionId = ev.action.id;
|
||||||
|
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await ev.action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-press confirmation to avoid accidental cancels
|
||||||
|
if (this.confirmPending.has(actionId)) {
|
||||||
|
// Second press — confirmed, cancel the print
|
||||||
|
clearTimeout(this.confirmPending.get(actionId)!);
|
||||||
|
this.confirmPending.delete(actionId);
|
||||||
|
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
try {
|
||||||
|
await client.cancelPrint();
|
||||||
|
await ev.action.setTitle("✓ Cancelled");
|
||||||
|
setTimeout(() => ev.action.setTitle("Cancel"), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
streamDeck.logger.error("PrintCancel: failed", err);
|
||||||
|
await ev.action.setTitle("⚠️ Error");
|
||||||
|
setTimeout(() => ev.action.setTitle("Cancel"), 3000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First press — ask for confirmation
|
||||||
|
await ev.action.setTitle("Press\nAgain!");
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
this.confirmPending.delete(actionId);
|
||||||
|
await ev.action.setTitle("Cancel");
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
this.confirmPending.set(actionId, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/actions/print-pause.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// src/actions/print-pause.ts
|
||||||
|
import streamDeck, { action, KeyDownEvent, SingletonAction, WillAppearEvent } from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { OctoPrintClient, OctoPrintSettings } from "../octoprint-client";
|
||||||
|
|
||||||
|
@action({ UUID: "com.octoprint.monitor.print-pause" })
|
||||||
|
export class PrintPauseAction extends SingletonAction {
|
||||||
|
override async onWillAppear(ev: WillAppearEvent): Promise<void> {
|
||||||
|
await this.updateTitle(ev.action, ev.payload.settings as unknown as OctoPrintSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onKeyDown(ev: KeyDownEvent): Promise<void> {
|
||||||
|
const settings = ev.payload.settings as unknown as OctoPrintSettings;
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await ev.action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const printer = await client.getPrinterState();
|
||||||
|
|
||||||
|
if (!printer.state.flags.printing && !printer.state.flags.paused) {
|
||||||
|
await ev.action.setTitle("Not\nPrinting");
|
||||||
|
setTimeout(() => ev.action.setTitle("Pause /\nResume"), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.pausePrint();
|
||||||
|
await ev.action.setTitle(printer.state.flags.paused ? "Resuming…" : "Pausing…");
|
||||||
|
|
||||||
|
// Refresh title after a moment
|
||||||
|
setTimeout(() => this.updateTitle(ev.action, settings), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
streamDeck.logger.error("PrintPause: failed", err);
|
||||||
|
await ev.action.setTitle("⚠️ Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateTitle(action: any, settings: OctoPrintSettings): Promise<void> {
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
const printer = await client.getPrinterState();
|
||||||
|
if (printer.state.flags.paused) {
|
||||||
|
await action.setTitle("▶ Resume");
|
||||||
|
} else if (printer.state.flags.printing) {
|
||||||
|
await action.setTitle("⏸ Pause");
|
||||||
|
} else {
|
||||||
|
await action.setTitle("Pause /\nResume");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await action.setTitle("Pause /\nResume");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/actions/printer-status.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// src/actions/printer-status.ts
|
||||||
|
import streamDeck, {
|
||||||
|
action,
|
||||||
|
DidReceiveSettingsEvent,
|
||||||
|
KeyDownEvent,
|
||||||
|
SingletonAction,
|
||||||
|
WillAppearEvent,
|
||||||
|
WillDisappearEvent,
|
||||||
|
} from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { OctoPrintClient, OctoPrintSettings, formatTime } from "../octoprint-client";
|
||||||
|
|
||||||
|
interface PrinterStatusSettings extends OctoPrintSettings {
|
||||||
|
showTemperature: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action({ UUID: "com.octoprint.monitor.printer-status" })
|
||||||
|
export class PrinterStatusAction extends SingletonAction {
|
||||||
|
private pollingIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
||||||
|
|
||||||
|
override async onWillAppear(ev: WillAppearEvent): Promise<void> {
|
||||||
|
await this.startPolling(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onWillDisappear(ev: WillDisappearEvent): Promise<void> {
|
||||||
|
this.stopPolling(ev.action.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise<void> {
|
||||||
|
this.stopPolling(ev.action.id);
|
||||||
|
await this.startPolling(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onKeyDown(ev: KeyDownEvent): Promise<void> {
|
||||||
|
// Refresh immediately on key press
|
||||||
|
await this.updateKey(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startPolling(action: any, rawSettings: any): Promise<void> {
|
||||||
|
const settings = rawSettings as PrinterStatusSettings;
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await action.setTitle("Configure\nPlugin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update immediately
|
||||||
|
await this.updateKey(action, settings);
|
||||||
|
|
||||||
|
// Poll every 5 seconds
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await this.updateKey(action, settings);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.pollingIntervals.set(action.id, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPolling(actionId: string): void {
|
||||||
|
const interval = this.pollingIntervals.get(actionId);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
this.pollingIntervals.delete(actionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateKey(action: any, rawSettings: any): Promise<void> {
|
||||||
|
const settings = rawSettings as PrinterStatusSettings;
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await action.setTitle("Not\nConfigured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [printer, job] = await Promise.all([
|
||||||
|
client.getPrinterState(),
|
||||||
|
client.getJobState(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stateText = printer.state.text;
|
||||||
|
const flags = printer.state.flags;
|
||||||
|
|
||||||
|
let title = "";
|
||||||
|
|
||||||
|
if (flags.printing) {
|
||||||
|
const pct = job.progress.completion ?? 0;
|
||||||
|
const timeLeft = formatTime(job.progress.printTimeLeft);
|
||||||
|
const fileName = job.job.file.name ?? "Unknown";
|
||||||
|
const shortName = fileName.length > 10 ? fileName.substring(0, 9) + "…" : fileName;
|
||||||
|
title = `${Math.round(pct)}%\n${timeLeft}\n${shortName}`;
|
||||||
|
|
||||||
|
if (settings.showTemperature && printer.temperature?.tool0) {
|
||||||
|
const temp = Math.round(printer.temperature.tool0.actual);
|
||||||
|
title = `${Math.round(pct)}%\n${temp}°C\n${timeLeft}`;
|
||||||
|
}
|
||||||
|
} else if (flags.paused) {
|
||||||
|
title = "PAUSED";
|
||||||
|
} else if (flags.operational) {
|
||||||
|
title = "Ready";
|
||||||
|
if (settings.showTemperature && printer.temperature?.tool0) {
|
||||||
|
const hotend = Math.round(printer.temperature.tool0.actual);
|
||||||
|
const bed = printer.temperature.bed ? Math.round(printer.temperature.bed.actual) : 0;
|
||||||
|
title = `Ready\n🔥${hotend}°\n⬛${bed}°`;
|
||||||
|
}
|
||||||
|
} else if (flags.closedOrError) {
|
||||||
|
title = "Offline";
|
||||||
|
} else {
|
||||||
|
title = stateText;
|
||||||
|
}
|
||||||
|
|
||||||
|
await action.setTitle(title);
|
||||||
|
} catch (err) {
|
||||||
|
await action.setTitle("⚠️ Error");
|
||||||
|
streamDeck.logger.error("PrinterStatus: failed to fetch", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/actions/temperature.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// src/actions/temperature.ts
|
||||||
|
import streamDeck, {
|
||||||
|
action,
|
||||||
|
DidReceiveSettingsEvent,
|
||||||
|
KeyDownEvent,
|
||||||
|
SingletonAction,
|
||||||
|
WillAppearEvent,
|
||||||
|
WillDisappearEvent,
|
||||||
|
} from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { OctoPrintClient, OctoPrintSettings } from "../octoprint-client";
|
||||||
|
|
||||||
|
@action({ UUID: "com.octoprint.monitor.temperature" })
|
||||||
|
export class TemperatureAction extends SingletonAction {
|
||||||
|
private pollingIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
||||||
|
|
||||||
|
override async onWillAppear(ev: WillAppearEvent): Promise<void> {
|
||||||
|
await this.startPolling(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onWillDisappear(ev: WillDisappearEvent): Promise<void> {
|
||||||
|
this.stopPolling(ev.action.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise<void> {
|
||||||
|
this.stopPolling(ev.action.id);
|
||||||
|
await this.startPolling(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onKeyDown(ev: KeyDownEvent): Promise<void> {
|
||||||
|
await this.updateKey(ev.action, ev.payload.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startPolling(action: any, rawSettings: any): Promise<void> {
|
||||||
|
const settings = rawSettings as OctoPrintSettings;
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateKey(action, settings);
|
||||||
|
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await this.updateKey(action, settings);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.pollingIntervals.set(action.id, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPolling(actionId: string): void {
|
||||||
|
const interval = this.pollingIntervals.get(actionId);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
this.pollingIntervals.delete(actionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateKey(action: any, rawSettings: any): Promise<void> {
|
||||||
|
const settings = rawSettings as OctoPrintSettings;
|
||||||
|
if (!settings?.host || !settings?.apiKey) {
|
||||||
|
await action.setTitle("Configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new OctoPrintClient(settings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const printer = await client.getPrinterState();
|
||||||
|
|
||||||
|
if (!printer.temperature) {
|
||||||
|
await action.setTitle("No Temp\nData");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotend = printer.temperature.tool0;
|
||||||
|
const bed = printer.temperature.bed;
|
||||||
|
|
||||||
|
const hotendStr = hotend
|
||||||
|
? `🔥${Math.round(hotend.actual)}/${Math.round(hotend.target)}°`
|
||||||
|
: "🔥 --";
|
||||||
|
|
||||||
|
const bedStr = bed
|
||||||
|
? `⬛${Math.round(bed.actual)}/${Math.round(bed.target)}°`
|
||||||
|
: "⬛ --";
|
||||||
|
|
||||||
|
await action.setTitle(`${hotendStr}\n${bedStr}`);
|
||||||
|
} catch (err) {
|
||||||
|
streamDeck.logger.error("Temperature: failed to fetch", err);
|
||||||
|
await action.setTitle("⚠️ Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/octoprint-client.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// src/octoprint-client.ts
|
||||||
|
// OctoPrint REST API client
|
||||||
|
|
||||||
|
export interface OctoPrintSettings {
|
||||||
|
host: string; // e.g. "http://octopi.local" or "http://192.168.1.100"
|
||||||
|
port: string; // e.g. "80" or "5000"
|
||||||
|
apiKey: string; // OctoPrint API key
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrinterState {
|
||||||
|
state: {
|
||||||
|
text: string;
|
||||||
|
flags: {
|
||||||
|
operational: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
printing: boolean;
|
||||||
|
pausing: boolean;
|
||||||
|
cancelling: boolean;
|
||||||
|
sdReady: boolean;
|
||||||
|
error: boolean;
|
||||||
|
ready: boolean;
|
||||||
|
closedOrError: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
temperature?: {
|
||||||
|
tool0?: { actual: number; target: number };
|
||||||
|
bed?: { actual: number; target: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobState {
|
||||||
|
job: {
|
||||||
|
file: { name: string | null };
|
||||||
|
estimatedPrintTime: number | null;
|
||||||
|
};
|
||||||
|
progress: {
|
||||||
|
completion: number | null;
|
||||||
|
printTimeLeft: number | null;
|
||||||
|
printTime: number | null;
|
||||||
|
};
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OctoPrintClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private apiKey: string;
|
||||||
|
|
||||||
|
constructor(settings: OctoPrintSettings) {
|
||||||
|
const port = settings.port ? `:${settings.port}` : "";
|
||||||
|
this.baseUrl = `${settings.host}${port}/api`;
|
||||||
|
this.apiKey = settings.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get headers(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
"X-Api-Key": this.apiKey,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrinterState(): Promise<PrinterState> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/printer`, {
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
return res.json() as Promise<PrinterState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJobState(): Promise<JobState> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/job`, {
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
return res.json() as Promise<JobState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async pausePrint(): Promise<void> {
|
||||||
|
await fetch(`${this.baseUrl}/job`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify({ command: "pause", action: "toggle" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelPrint(): Promise<void> {
|
||||||
|
await fetch(`${this.baseUrl}/job`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify({ command: "cancel" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async homeAxes(axes: string[] = ["x", "y", "z"]): Promise<void> {
|
||||||
|
await fetch(`${this.baseUrl}/printer/printhead`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify({ command: "home", axes }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTemperature(tool: "tool0" | "bed", target: number): Promise<void> {
|
||||||
|
if (tool === "bed") {
|
||||||
|
await fetch(`${this.baseUrl}/printer/bed`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify({ command: "target", target }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fetch(`${this.baseUrl}/printer/tool`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify({ command: "target", targets: { [tool]: target } }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${this.baseUrl}/version`, { headers: this.headers });
|
||||||
|
return res.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTime(seconds: number | null): string {
|
||||||
|
if (seconds === null || seconds === undefined) return "--:--";
|
||||||
|
const h = Math.floor(seconds / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (h > 0) return `${h}h ${m}m`;
|
||||||
|
return `${m}m`;
|
||||||
|
}
|
||||||
18
src/plugin.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// src/plugin.ts
|
||||||
|
import streamDeck from "@elgato/streamdeck";
|
||||||
|
|
||||||
|
import { PrinterStatusAction } from "./actions/printer-status";
|
||||||
|
import { PrintPauseAction } from "./actions/print-pause";
|
||||||
|
import { PrintCancelAction } from "./actions/print-cancel";
|
||||||
|
import { TemperatureAction } from "./actions/temperature";
|
||||||
|
import { HomeAxesAction } from "./actions/home-axes";
|
||||||
|
|
||||||
|
// Register all actions
|
||||||
|
streamDeck.actions.registerAction(new PrinterStatusAction() as any);
|
||||||
|
streamDeck.actions.registerAction(new PrintPauseAction() as any);
|
||||||
|
streamDeck.actions.registerAction(new PrintCancelAction() as any);
|
||||||
|
streamDeck.actions.registerAction(new TemperatureAction() as any);
|
||||||
|
streamDeck.actions.registerAction(new HomeAxesAction() as any);
|
||||||
|
|
||||||
|
// Connect to Stream Deck
|
||||||
|
streamDeck.connect();
|
||||||
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"customConditions": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"noImplicitOverride": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||