diff --git a/docs/widgets/services/prusalink.md b/docs/widgets/services/prusalink.md
new file mode 100644
index 00000000..1da32c10
--- /dev/null
+++ b/docs/widgets/services/prusalink.md
@@ -0,0 +1,15 @@
+---
+title: PrusaLink
+description: PrusaLink Widget Configuration
+---
+
+[PrusaLink](https://github.com/prusa3d/Prusa-Link-Web)
+
+Allowed fields: `["print_progress", "print_time", "print_time_left"]`.
+
+```yaml
+widget:
+ type: prusalink
+ url: http://prusalink.host
+ key: mytokenhere # see https://help.prusa3d.com/article/sending-g-codes-to-printer-via-network-prusaconnect-prusalink-octoprint_196761
+```
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index b0f60a6d..1542e0e8 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -808,5 +808,10 @@
"netdata": {
"warnings": "Warnings",
"criticals": "Criticals"
+ },
+ "prusalink": {
+ "print_progress": "Progress",
+ "print_time": "Print time",
+ "print_time_left": "Remaining time"
}
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 497d6407..1675e2f2 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -110,6 +110,7 @@ const components = {
watchtower: dynamic(() => import("./watchtower/component")),
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
xteve: dynamic(() => import("./xteve/component")),
+ prusalink: dynamic(() => import("./prusalink/component")),
};
export default components;
diff --git a/src/widgets/prusalink/component.jsx b/src/widgets/prusalink/component.jsx
new file mode 100644
index 00000000..83577337
--- /dev/null
+++ b/src/widgets/prusalink/component.jsx
@@ -0,0 +1,72 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+function secondsToTimeObj(seconds) {
+ return {
+ seconds: seconds % 60,
+ minutes: Math.floor((seconds / 60) % 60),
+ hours: Math.floor((seconds / 3600) % 60),
+ };
+}
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+ const { data: prusalinkStats, error: prusalinkError } = useWidgetAPI(widget, "prusalink");
+ const isPrinting = prusalinkStats?.state === "Printing";
+
+ if (prusalinkError) {
+ return ;
+ }
+
+ if (!isPrinting) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const progress = prusalinkStats.progress * 100;
+ const printTime = secondsToTimeObj(prusalinkStats.printTime);
+ const printTimeLeft = secondsToTimeObj(prusalinkStats.printTimeLeft);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/prusalink/proxy.js b/src/widgets/prusalink/proxy.js
new file mode 100644
index 00000000..54e6018b
--- /dev/null
+++ b/src/widgets/prusalink/proxy.js
@@ -0,0 +1,63 @@
+import { httpProxy } from "utils/proxy/http";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+
+const proxyName = "prusalinkProxyHandler";
+const logger = createLogger(proxyName);
+
+async function retrieveFromAPI(url, key) {
+ const headers = {
+ "content-type": "application/json",
+ "X-Api-Key": key,
+ };
+
+ const [status, , data] = await httpProxy(url, { headers });
+
+ if (status !== 200) {
+ throw new Error(`Error getting data from prusalink: ${status}. Data: ${data.toString()}`);
+ }
+
+ return JSON.parse(Buffer.from(data).toString());
+}
+
+export default async function prusalinkProxyHandler(req, res) {
+ const { group, service, endpoint } = req.query;
+
+ if (!group || !service) {
+ logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const widget = await getServiceWidget(group, service);
+
+ if (!widget) {
+ logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ if (!widget.key) {
+ logger.debug("Invalid or missing key for service '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Missing widget key" });
+ }
+
+ const apiURL = widgets[widget.type].api;
+
+ try {
+ const url = new URL(formatApiCall(apiURL, { endpoint, ...widget }));
+ const prusalinkData = await retrieveFromAPI(url, widget.key);
+
+ const prusalinkStats = {
+ state: prusalinkData.state,
+ progress: prusalinkData.progress?.completion,
+ printTime: prusalinkData.progress?.printTime,
+ printTimeLeft: prusalinkData.progress?.printTimeLeft,
+ };
+
+ return res.status(200).send(prusalinkStats);
+ } catch (e) {
+ logger.error(e.message);
+ return res.status(500).send({ error: { message: e.message } });
+ }
+}
diff --git a/src/widgets/prusalink/widget.js b/src/widgets/prusalink/widget.js
new file mode 100644
index 00000000..c2b8da0f
--- /dev/null
+++ b/src/widgets/prusalink/widget.js
@@ -0,0 +1,8 @@
+import prusalinkProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/api/job",
+ proxyHandler: prusalinkProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 553ce626..de27c203 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -103,6 +103,7 @@ import whatsupdocker from "./whatsupdocker/widget";
import xteve from "./xteve/widget";
import urbackup from "./urbackup/widget";
import romm from "./romm/widget";
+import prusalink from "./prusalink/widget";
const widgets = {
adguard,
@@ -212,6 +213,7 @@ const widgets = {
watchtower,
whatsupdocker,
xteve,
+ prusalink,
};
export default widgets;