Skip to content

Commit db5e852

Browse files
add job-preview image
Signed-off-by: Scott Hanson <scooter_seh@yahoo.com>
1 parent ec01852 commit db5e852

13 files changed

Lines changed: 202 additions & 27 deletions

File tree

bundles/org.openhab.binding.threedprinter/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ All three thing types expose the same set of channels.
6969
| `fan-speed` | Number | RW | Part-cooling fan speed percentage (0–100). Read support varies by firmware. |
7070
| `pause-resume` | Switch | RW | `ON` when the print is paused. Send `ON` to pause, `OFF` to resume. |
7171
| `cancel` | Switch | W | Send `ON` to cancel the current print. Resets to `OFF` automatically. |
72+
| `job-preview` | Image | R | Thumbnail image of the object being printed. Only populated when the sliced file contains embedded thumbnails. For OctoPrint, requires the [PrusaSlicer Thumbnails](https://plugins.octoprint.org/plugins/prusaslicerthumbnails/) plugin. |
7273

7374
## Full Example
7475

@@ -114,6 +115,7 @@ Number MK4_PrintSpeed "Print speed [%d %%]" { channel="threedpr
114115
Number MK4_FanSpeed "Fan speed [%d %%]" { channel="threedprinter:prusaprinter:mk4:fan-speed" }
115116
Switch MK4_PauseResume "Paused" { channel="threedprinter:prusaprinter:mk4:pause-resume" }
116117
Switch MK4_Cancel "Cancel print" { channel="threedprinter:prusaprinter:mk4:cancel" }
118+
Image MK4_Preview "Print preview" { channel="threedprinter:prusaprinter:mk4:job-preview" }
117119
```
118120

119121
### Sitemap
@@ -136,6 +138,7 @@ sitemap threedprinter label="3D Printers" {
136138
Slider item=MK4_FanSpeed minValue=0 maxValue=100 step=5
137139
Switch item=MK4_PauseResume label="Pause/Resume"
138140
Switch item=MK4_Cancel label="Cancel Print"
141+
Image item=MK4_Preview
139142
}
140143
}
141144
```

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/ThreedprinterBindingConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class ThreedprinterBindingConstants {
4444
public static final String CHANNEL_FAN_SPEED = "fan-speed";
4545
public static final String CHANNEL_PAUSE_RESUME = "pause-resume";
4646
public static final String CHANNEL_CANCEL = "cancel";
47+
public static final String CHANNEL_JOB_PREVIEW = "job-preview";
4748

4849
// Printer state values
4950
public static final String STATE_IDLE = "IDLE";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.threedprinter.internal.dto.klipper;
14+
15+
import java.util.List;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.eclipse.jdt.annotation.Nullable;
19+
20+
import com.google.gson.annotations.SerializedName;
21+
22+
/**
23+
* DTO for the Moonraker GET /server/files/metadata response.
24+
*
25+
* @author Scott Hanson - Initial contribution
26+
*/
27+
@NonNullByDefault
28+
public class KlipperMetadataResponse {
29+
30+
@SerializedName("result")
31+
public @Nullable KlipperMetadataResult result;
32+
33+
public static class KlipperMetadataResult {
34+
@SerializedName("thumbnails")
35+
public @Nullable List<KlipperThumbnail> thumbnails;
36+
}
37+
38+
public static class KlipperThumbnail {
39+
@SerializedName("relative_path")
40+
public String relativePath = "";
41+
42+
@SerializedName("width")
43+
public int width;
44+
45+
@SerializedName("height")
46+
public int height;
47+
48+
@SerializedName("size")
49+
public int size;
50+
}
51+
}

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/dto/klipper/KlipperObjectsResponse.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ public class KlipperObjectsResponse {
2828
@SerializedName("result")
2929
public @Nullable KlipperResult result;
3030

31-
@NonNullByDefault
3231
public static class KlipperResult {
3332
@SerializedName("status")
3433
public @Nullable KlipperStatus status;
3534
}
3635

37-
@NonNullByDefault
3836
public static class KlipperStatus {
3937
@SerializedName("extruder")
4038
public @Nullable KlipperHeater extruder;
@@ -55,7 +53,6 @@ public static class KlipperStatus {
5553
public @Nullable KlipperGcodeMove gcodeMove;
5654
}
5755

58-
@NonNullByDefault
5956
public static class KlipperHeater {
6057
@SerializedName("temperature")
6158
public double temperature;
@@ -64,7 +61,6 @@ public static class KlipperHeater {
6461
public double target;
6562
}
6663

67-
@NonNullByDefault
6864
public static class KlipperPrintStats {
6965
@SerializedName("state")
7066
public String state = "";
@@ -82,7 +78,6 @@ public static class KlipperPrintStats {
8278
public double filamentUsed;
8379
}
8480

85-
@NonNullByDefault
8681
public static class KlipperDisplayStatus {
8782
@SerializedName("progress")
8883
public double progress;
@@ -91,13 +86,11 @@ public static class KlipperDisplayStatus {
9186
public String message = "";
9287
}
9388

94-
@NonNullByDefault
9589
public static class KlipperFan {
9690
@SerializedName("speed")
9791
public double speed;
9892
}
9993

100-
@NonNullByDefault
10194
public static class KlipperGcodeMove {
10295
@SerializedName("speed_factor")
10396
public double speedFactor = 1.0;

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/dto/octoprint/OctoPrintJobResponse.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,13 @@ public class OctoPrintJobResponse {
3434
@SerializedName("state")
3535
public String state = "";
3636

37-
@NonNullByDefault
3837
public static class OctoPrintJob {
3938
@SerializedName("file")
4039
public @Nullable OctoPrintFile file;
4140

4241
@SerializedName("estimatedPrintTime")
4342
public double estimatedPrintTime;
4443

45-
@NonNullByDefault
4644
public static class OctoPrintFile {
4745
@SerializedName("name")
4846
public String name = "";
@@ -52,7 +50,6 @@ public static class OctoPrintFile {
5250
}
5351
}
5452

55-
@NonNullByDefault
5653
public static class OctoPrintProgress {
5754
@SerializedName("completion")
5855
public double completion;

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/dto/octoprint/OctoPrintPrinterResponse.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ public class OctoPrintPrinterResponse {
3131
@SerializedName("state")
3232
public @Nullable OctoPrintState state;
3333

34-
@NonNullByDefault
3534
public static class OctoPrintTemperature {
3635
@SerializedName("tool0")
3736
public @Nullable OctoPrintTempReading tool0;
3837

3938
@SerializedName("bed")
4039
public @Nullable OctoPrintTempReading bed;
4140

42-
@NonNullByDefault
4341
public static class OctoPrintTempReading {
4442
@SerializedName("actual")
4543
public double actual;
@@ -49,15 +47,13 @@ public static class OctoPrintTempReading {
4947
}
5048
}
5149

52-
@NonNullByDefault
5350
public static class OctoPrintState {
5451
@SerializedName("text")
5552
public String text = "";
5653

5754
@SerializedName("flags")
5855
public @Nullable OctoPrintStateFlags flags;
5956

60-
@NonNullByDefault
6157
public static class OctoPrintStateFlags {
6258
@SerializedName("printing")
6359
public boolean printing;

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/dto/prusa/PrusaStatusResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public class PrusaStatusResponse {
3131
@SerializedName("job")
3232
public @Nullable PrusaJobData job;
3333

34-
@NonNullByDefault
3534
public static class PrusaPrinterData {
3635
@SerializedName("state")
3736
public String state = "";
@@ -58,7 +57,6 @@ public static class PrusaPrinterData {
5857
public int flow = 100;
5958
}
6059

61-
@NonNullByDefault
6260
public static class PrusaJobData {
6361
@SerializedName("progress")
6462
public double progress;
@@ -72,13 +70,15 @@ public static class PrusaJobData {
7270
@SerializedName("file")
7371
public @Nullable PrusaFileData file;
7472

75-
@NonNullByDefault
7673
public static class PrusaFileData {
7774
@SerializedName("display_name")
7875
public String displayName = "";
7976

8077
@SerializedName("name")
8178
public String name = "";
79+
80+
@SerializedName("path")
81+
public String path = "";
8282
}
8383
}
8484
}

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/handler/AbstractPrinterHandler.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ protected int httpPut(String url, String apiKey, String jsonBody) {
146146
}
147147
}
148148

149+
/**
150+
* Sends an HTTP GET and returns the raw response bytes, or null on failure.
151+
*/
152+
protected byte @Nullable [] httpGetBytes(String url, String apiKey) {
153+
try {
154+
Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(10, TimeUnit.SECONDS);
155+
if (!apiKey.isBlank()) {
156+
request.header("X-Api-Key", apiKey);
157+
}
158+
ContentResponse response = request.send();
159+
if (response.getStatus() == 200) {
160+
return response.getContent();
161+
}
162+
logger.debug("GET bytes {} returned HTTP {}", url, response.getStatus());
163+
return null;
164+
} catch (Exception e) {
165+
logger.debug("GET bytes {} failed: {}", url, e.getMessage());
166+
return null;
167+
}
168+
}
169+
149170
protected void markOffline(String message) {
150171
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
151172
}

bundles/org.openhab.binding.threedprinter/src/main/java/org/openhab/binding/threedprinter/internal/handler/KlipperHandler.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@
1414

1515
import static org.openhab.binding.threedprinter.internal.ThreedprinterBindingConstants.*;
1616

17+
import java.net.URLEncoder;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Arrays;
20+
import java.util.Comparator;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
1724
import javax.measure.quantity.Temperature;
1825

1926
import org.eclipse.jdt.annotation.NonNullByDefault;
2027
import org.eclipse.jdt.annotation.Nullable;
2128
import org.eclipse.jetty.client.HttpClient;
2229
import org.openhab.binding.threedprinter.internal.config.KlipperConfiguration;
30+
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperMetadataResponse;
31+
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperMetadataResponse.KlipperMetadataResult;
32+
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperMetadataResponse.KlipperThumbnail;
2333
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperObjectsResponse;
2434
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperObjectsResponse.KlipperDisplayStatus;
2535
import org.openhab.binding.threedprinter.internal.dto.klipper.KlipperObjectsResponse.KlipperFan;
@@ -31,6 +41,7 @@
3141
import org.openhab.core.library.types.DecimalType;
3242
import org.openhab.core.library.types.OnOffType;
3343
import org.openhab.core.library.types.QuantityType;
44+
import org.openhab.core.library.types.RawType;
3445
import org.openhab.core.library.types.StringType;
3546
import org.openhab.core.library.unit.SIUnits;
3647
import org.openhab.core.thing.ChannelUID;
@@ -55,6 +66,7 @@ public class KlipperHandler extends AbstractPrinterHandler {
5566
private static final String QUERY_URL_SUFFIX = "/printer/objects/query?extruder&heater_bed&print_stats&display_status&fan&gcode_move";
5667

5768
private @Nullable KlipperConfiguration config;
69+
private String lastPreviewFilename = "";
5870

5971
public KlipperHandler(Thing thing, HttpClient httpClient) {
6072
super(thing, httpClient);
@@ -131,6 +143,15 @@ protected void refresh() {
131143
updateState(CHANNEL_PAUSE_RESUME, OnOffType.from(STATE_PAUSED.equals(mappedState)));
132144
updateState(CHANNEL_JOB_NAME, new StringType(stats.filename));
133145
updateState(CHANNEL_TIME_ELAPSED, new DecimalType((long) stats.printDuration));
146+
147+
if (!stats.filename.isBlank()) {
148+
if (!stats.filename.equals(lastPreviewFilename)) {
149+
lastPreviewFilename = stats.filename;
150+
fetchAndUpdatePreview(baseUrl, cfg.apiKey, stats.filename);
151+
}
152+
} else {
153+
lastPreviewFilename = "";
154+
}
134155
}
135156

136157
KlipperDisplayStatus display = status.displayStatus;
@@ -156,6 +177,38 @@ protected void refresh() {
156177
}
157178
}
158179

180+
private void fetchAndUpdatePreview(String baseUrl, String apiKey, String filename) {
181+
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
182+
String metaJson = httpGet(baseUrl + "/server/files/metadata?filename=" + encodedFilename, apiKey);
183+
if (metaJson == null) {
184+
return;
185+
}
186+
KlipperMetadataResponse meta = gson.fromJson(metaJson, KlipperMetadataResponse.class);
187+
if (meta == null) {
188+
return;
189+
}
190+
KlipperMetadataResult metaResult = meta.result;
191+
if (metaResult == null) {
192+
return;
193+
}
194+
List<KlipperThumbnail> thumbnails = metaResult.thumbnails;
195+
if (thumbnails == null || thumbnails.isEmpty()) {
196+
return;
197+
}
198+
@Nullable
199+
KlipperThumbnail best = thumbnails.stream().max(Comparator.comparingInt(t -> t.size)).orElse(null);
200+
if (best == null || best.relativePath.isBlank()) {
201+
return;
202+
}
203+
// Encode each path segment individually to preserve the directory separator
204+
String encodedPath = Arrays.stream(best.relativePath.split("/"))
205+
.map(s -> URLEncoder.encode(s, StandardCharsets.UTF_8)).collect(Collectors.joining("/"));
206+
byte @Nullable [] bytes = httpGetBytes(baseUrl + "/server/files/gcodes/" + encodedPath, apiKey);
207+
if (bytes != null && bytes.length > 0) {
208+
updateState(CHANNEL_JOB_PREVIEW, new RawType(bytes, "image/png"));
209+
}
210+
}
211+
159212
@Override
160213
public void handleCommand(ChannelUID channelUID, Command command) {
161214
KlipperConfiguration cfg = config;

0 commit comments

Comments
 (0)