[bambulab] Initial contribution (#18369)

* Init BambuLab

Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
pull/18390/head
Martin 2025-03-12 22:14:31 +01:00 committed by GitHub
parent 5ef6840d22
commit 9ac731580e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1712 additions and 2 deletions

View File

@ -38,6 +38,7 @@
/bundles/org.openhab.binding.automower/ @maxpg
/bundles/org.openhab.binding.avmfritz/ @cweitkamp
/bundles/org.openhab.binding.awattar/ @Wolfgang1966
/bundles/org.openhab.binding.bambulab/ @magx2
/bundles/org.openhab.binding.benqprojector/ @mlobstein
/bundles/org.openhab.binding.bigassfan/ @mhilbush
/bundles/org.openhab.binding.bluetooth/ @cdjackson @cpmeister

View File

@ -181,6 +181,11 @@
<artifactId>org.openhab.binding.awattar</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bambulab</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.benqprojector</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,172 @@
# BambuLab Binding
This addon supports connecting with BambuLab 3D printers in local mode.
While cloud mode is theoretically possible, it is not supported by the addon developers.
## Cloud Mode
Cloud mode is possible but not officially supported by the addon developers.
To use cloud mode, follow these steps:
### Find Username
Log in to Maker World and visit [my-preferences](https://makerworld.com/api/v1/design-user-service/my/preference) to retrieve a JSON response containing your data.
The relevant field is `uid`, which represents the unique ID of your account.
Use this value as the `username` in the configuration (advanced field) with the prefix `u_`.
### Access Token
To obtain an access token, follow these steps:
1. Log in using your email and password.
2. Confirm the login using a token received via email.
#### Step 1: Login with Email and Password
```shell
curl -X POST "https://api.bambulab.com/v1/user-service/user/login" \
-H "Content-Type: application/json" \
-d '{
"account": "you@email.io",
"password": "superduperpassword123"
}'
```
#### Step 2: Confirm Login with Token from Email
```shell
curl -X POST "https://api.bambulab.com/v1/user-service/user/login" \
-H "Content-Type: application/json" \
-d '{
"account": "you@email.io",
"code": "123456"
}'
```
You will receive a long access code in the response. Copy it and use it as the `accessCode` parameter.
**Note:** This access code expires after three months. When it expires, repeat the process to obtain a new one.
### Hostname
Use `us.mqtt.bambulab.com` as the hostname.
## Supported Things
- `printer`: Represents a BambuLab 3D printer.
## Thing Configuration
| Parameter | Type | Required | Description |
|--------------|---------|----------|-------------------------------------------------------------------------------------------------|
| `serial` | Text | Yes | Unique serial number of the printer. |
| `scheme` | Text | No | URI scheme. (Advanced) |
| `hostname` | Text | Yes | IP address of the printer or `us.mqtt.bambulab.com` for cloud mode. |
| `port` | Integer | No | URI port. (Advanced) |
| `username` | Text | No | `bblp` for local mode or your Bambu Lab user (starting with `u_`). (Advanced) |
| `accessCode` | Text | Yes | Access code for the printer. The method of obtaining this varies between local and cloud modes. |
## Channels
| Channel ID | Type | Description |
|---------------------------|---------------------|------------------------------------------------------------------|
| `nozzle-temperature` | Temperature Channel | Current temperature of the nozzle. |
| `nozzle-target-temperature` | Temperature Channel | Target temperature of the nozzle. |
| `bed-temperature` | Temperature Channel | Current temperature of the heated bed. |
| `bed-target-temperature` | Temperature Channel | Target temperature of the heated bed. |
| `chamber-temperature` | Temperature Channel | Current temperature inside the printer chamber. |
| `mc-print-stage` | String Channel | Current stage of the print process. |
| `mc-percent` | Percent Channel | Percentage of the print completed. |
| `mc-remaining-time` | Number Channel | Estimated time remaining for the print (in seconds). |
| `wifi-signal` | WiFi Channel | Current WiFi signal strength. |
| `bed-type` | String Channel | Type of the printer's heated bed. |
| `gcode-file` | String Channel | Name of the currently loaded G-code file. |
| `gcode-state` | String Channel | Current state of the G-code execution. |
| `reason` | String Channel | Reason for pausing or stopping the print. |
| `result` | String Channel | Final result or status of the print job. |
| `gcode-file-prepare-percent` | Percent Channel | Percentage of G-code file preparation completed. |
| `big-fan1-speed` | Number Channel | Speed of the first large cooling fan (RPM). |
| `big-fan2-speed` | Number Channel | Speed of the second large cooling fan (RPM). |
| `heat-break-fan-speed` | Number Channel | Speed of the heat break cooling fan (RPM). |
| `layer-num` | Number Channel | Current layer being printed. |
| `speed-level` | Number Channel | Current speed setting of the print job. |
| `time-laps` | Boolean Channel | Indicates whether time-lapse recording is enabled. |
| `use-ams` | Boolean Channel | Indicates whether the Automatic Material System (AMS) is active. |
| `vibration-calibration` | Boolean Channel | Indicates whether vibration calibration has been performed. |
| `led-chamber` | On/Off Command | Controls the LED lighting inside the printer chamber. |
| `led-work` | On/Off Command | Controls the LED lighting for the work area. |
## Full Example
### `bambulab.things` Example
```java
Thing bambulab:printer:myprinter "My BambuLab Printer" @ "3D Printing Area" [
serial="ABC123456789",
hostname="192.168.1.100",
accessCode="your_access_code_here"
]
```
### `bambulab.items` Exmaple
```java
Number:Temperature NozzleTemperature "Nozzle Temperature [%.1f °C]" { channel="bambulab:printer:myprinter:nozzle-temperature" }
Number:Temperature BedTemperature "Bed Temperature [%.1f °C]" { channel="bambulab:printer:myprinter:bed-temperature" }
String PrintStage "Print Stage [%s]" { channel="bambulab:printer:myprinter:mc-print-stage" }
Switch LedChamber "Chamber LED" { channel="bambulab:printer:myprinter:led-chamber" }
```
## Actions
The printer thing supports actions:
```java
rule "test"
when
/* when */
then
val actions = getActions("bambulab", "bambulab:printer:as8af03m38")
if(actions !==null){
// Refresh all channels
actions.refreshChannels()
actions.sendCommand("Pushing:1:1")
}
end
```
### `refreshChannels`
Reports the complete status of the printer.
This is unnecessary for the X1 series since it already transmits the full object each time.
However, the P1 series only sends the values that have been updated compared to the previous report.
As a rule of thumb, refrain from executing this command at intervals less than 5 minutes on the P1P, as it may cause lag due to its hardware limitations.
### `sendCommand`
The `sendCommand` method expects a string command in the format:
```
CommandType:Parameter1:Parameter2:...
```
#### Possible Commands:
| Command Type | Parameters | Description |
|----------------------|------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| `Pushing` | `version(int)`,`pushTarget(int)` (optional) | Sends a push command. |
| `Print` | `START` / `STOP` / `PAUSE` | Controls the print job. |
| `ChangeFilament` | `target(int)`,`currentTemperature(int))`,`targetTemperature(int)` | Changes filament using. |
| `AmsUserSetting` | `amsId(int)`,`startupReadOption(boolean)`,`trayReadOption(boolean)` | Sets AMS user settings. |
| `AmsFilamentSetting` | `amsId(int)`,`trayId(int)`,`trayInfoIdx(string)`,`trayColor(string)`,`nozzleTempMin(int)`,`nozzleTempMax(int)`,`trayType(string)` | Configures filament settings. |
| `AmsControl` | ` RESUME` / `RESET` / `PAUSE` | Sends an AMS control command. |
| `PrintSpeed` | `SILENT` / `STANDARD` / `SPORT` / `LUDICROUS` | Adjusts print speed. |
| `GCodeFile` | `filename(string)` | Loads a G-code file. |
| `GCodeLine` | `userId(string)\nlines(string...)` | Sends multiple G-code lines. Lines are enter (`\n`) separated |
| `LedControl` | (`CHAMBER_LIGHT` / `WORK_LIGHT`),(`ON` / `OFF` / `FLASHING`),`ledOnTime(int)?`,`ledOffTime(int)?`,`loopTimes(int)?`,`intervalTime(int)?` | Controls LED lighting. |
| `System` | `GET_ACCESS_CODE` | Executes a system command. |
| `IpCamRecord` | `enable(boolean)` | Starts or stops IP camera recording. |
| `Info` | `GET_VERSION` | Sends a info command. |
| `IpCamTimelaps` | `enable(boolean)` | Enables or disables timelapse recording. |
| `XCamControl` | (`FIRST_LAYER_INSPECTOR` / `SPAGHETTI_DETECTOR`),`control(boolean)`,`printHalt(boolean)` | Controls XCam settings. |

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.bambulab</artifactId>
<name>openHAB Add-ons :: Bundles :: BambuLab Binding</name>
<dependencies>
<dependency>
<groupId>pl.grzeslowski</groupId>
<artifactId>JBambuAPI</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<!-- TEST -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bambulab-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-bambulab" description="BambuLab Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bambulab/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BambuLabBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class BambuLabBindingConstants {
public static final String BINDING_ID = "bambulab";
// List of all Thing Type UIDs
public static final ThingTypeUID PRINTER_THING_TYPE = new ThingTypeUID(BINDING_ID, "printer");
@SuppressWarnings("StaticMethodOnlyUsedInOneClass")
public enum Channel {
CHANNEL_NOZZLE_TEMPERATURE("nozzle-temperature"),
CHANNEL_NOZZLE_TARGET_TEMPERATURE("nozzle-target-temperature"),
CHANNEL_BED_TEMPERATURE("bed-temperature"),
CHANNEL_BED_TARGET_TEMPERATURE("bed-target-temperature"),
CHANNEL_CHAMBER_TEMPERATURE("chamber-temperature"),
CHANNEL_MC_PRINT_STAGE("mc-print-stage"),
CHANNEL_MC_PERCENT("mc-percent"),
CHANNEL_MC_REMAINING_TIME("mc-remaining-time"),
CHANNEL_WIFI_SIGNAL("wifi-signal"),
CHANNEL_BED_TYPE("bed-type"),
CHANNEL_GCODE_FILE("gcode-file"),
CHANNEL_GCODE_STATE("gcode-state"),
CHANNEL_REASON("reason"),
CHANNEL_RESULT("result"),
CHANNEL_GCODE_FILE_PREPARE_PERCENT("gcode-file-prepare-percent"),
CHANNEL_BIG_FAN_1_SPEED("big-fan1-speed"),
CHANNEL_BIG_FAN_2_SPEED("big-fan2-speed"),
CHANNEL_HEAT_BREAK_FAN_SPEED("heat-break-fan-speed"),
CHANNEL_LAYER_NUM("layer-num"),
CHANNEL_SPEED_LEVEL("speed-level"),
CHANNEL_TIME_LAPS("time-laps"),
CHANNEL_USE_AMS("use-ams"),
CHANNEL_VIBRATION_CALIBRATION("vibration-calibration"),
CHANNEL_LED_CHAMBER_LIGHT("led-chamber", true),
CHANNEL_LED_WORK_LIGHT("led-work", true);
private final String name;
private final boolean supportCommand;
Channel(String name, boolean supportCommand) {
this.name = name;
this.supportCommand = supportCommand;
}
private Channel(String name) {
this(name, false);
}
public String getName() {
return name;
}
public boolean isSupportCommand() {
return supportCommand;
}
@Override
public String toString() {
return name;
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.PRINTER_THING_TYPE;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link BambuLabHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.bambulab", service = ThingHandlerFactory.class)
public class BambuLabHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PRINTER_THING_TYPE);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PRINTER_THING_TYPE.equals(thingTypeUID)) {
return new PrinterHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,266 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Integer.parseInt;
import static java.util.Arrays.copyOfRange;
import static java.util.Objects.requireNonNull;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.BINDING_ID;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedMode.FLASHING;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsControlCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsFilamentSettingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsUserSettingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.ChangeFilamentCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.Command;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.GCodeFileCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.GCodeLineCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.InfoCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.IpCamRecordCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.IpCamTimelapsCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedMode;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedNode;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.PrintCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.PrintSpeedCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.PushingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.SystemCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.XCamControlCommand;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@ThingActionsScope(name = BINDING_ID)
@NonNullByDefault
public class PrinterActions implements ThingActions {
private @Nullable PrinterHandler handler;
@Override
public void setThingHandler(ThingHandler thingHandler) {
this.handler = (PrinterHandler) thingHandler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@RuleAction(label = "@text/action.sendCommand.label", description = "@text/action.sendCommand.description")
public void sendCommand(
@ActionInput(name = "time", label = "@text/action.sendCommand.commandLabel", description = "@text/action.sendCommand.commandDescription") String stringCommand) {
var localHandler = handler;
if (localHandler == null) {
return;
}
var command = parseCommand(stringCommand);
localHandler.sendCommand(command);
}
private Command parseCommand(String stringCommand) {
var split = stringCommand.split(":");
if (split.length <= 1) {
throw new IllegalArgumentException("Command too short, class name not passed. Command: " + stringCommand);
}
var commandName = split[0] + "Command";
var tail = tail(split);
if (commandName.equals(InfoCommand.class.getSimpleName())) {
return parseInfoCommand(tail);
}
if (commandName.equals(PushingCommand.class.getSimpleName())) {
return parsePushingCommand(tail);
}
if (commandName.equals(PrintCommand.class.getSimpleName())) {
return parsePrintCommand(tail);
}
if (commandName.equals(ChangeFilamentCommand.class.getSimpleName())) {
return parseChangeFilamentCommand(tail);
}
if (commandName.equals(AmsUserSettingCommand.class.getSimpleName())) {
return parseAmsUserSettingCommand(tail);
}
if (commandName.equals(AmsFilamentSettingCommand.class.getSimpleName())) {
return parseAmsFilamentSettingCommand(tail);
}
if (commandName.equals(AmsControlCommand.class.getSimpleName())) {
return parseAmsControlCommand(tail);
}
if (commandName.equals(PrintSpeedCommand.class.getSimpleName())) {
return parsePrintSpeedCommand(tail);
}
if (commandName.equals(GCodeFileCommand.class.getSimpleName())) {
return parseGCodeFileCommand(tail);
}
if (commandName.equals(GCodeLineCommand.class.getSimpleName())) {
var gcodeLineSplit = stringCommand.split(":", 2);
requireLength(gcodeLineSplit, 2);
return parseGCodeLineCommand(gcodeLineSplit[1]);
}
if (commandName.equals(LedControlCommand.class.getSimpleName())) {
return parseLedControlCommand(tail);
}
if (commandName.equals(SystemCommand.class.getSimpleName())) {
return parseSystemCommand(tail);
}
if (commandName.equals(IpCamRecordCommand.class.getSimpleName())) {
return parseIpCamRecordCommand(tail);
}
if (commandName.equals(IpCamTimelapsCommand.class.getSimpleName())) {
return parseIpCamTimelapsCommand(tail);
}
if (commandName.equals(XCamControlCommand.class.getSimpleName())) {
return parseXCamControlCommand(tail);
}
throw new IllegalArgumentException("Unknown command name: " + commandName);
}
private String[] tail(String[] command) {
return copyOfRange(command, 1, command.length);
}
private void requireLength(String[] commandLine, int length) {
if (commandLine.length != length) {
throw new IllegalArgumentException("Command line length does not match! Should be %s, but was %s!"
.formatted(length, commandLine.length));
}
}
private InfoCommand parseInfoCommand(String[] commandLine) {
requireLength(commandLine, 1);
return InfoCommand.valueOf(commandLine[0]);
}
private PushingCommand parsePushingCommand(String[] commandLine) {
if (commandLine.length == 0) {
return PushingCommand.defaultPushingCommand();
}
requireLength(commandLine, 2);
return new PushingCommand(parseInt(commandLine[0]), parseInt(commandLine[1]));
}
private PrintCommand parsePrintCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrintCommand.valueOf(commandLine[0]);
}
private ChangeFilamentCommand parseChangeFilamentCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new ChangeFilamentCommand(parseInt(commandLine[0]), parseInt(commandLine[1]), parseInt(commandLine[2]));
}
private AmsUserSettingCommand parseAmsUserSettingCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new AmsUserSettingCommand(parseInt(commandLine[0]), parseBoolean(commandLine[1]),
parseBoolean(commandLine[2]));
}
private AmsFilamentSettingCommand parseAmsFilamentSettingCommand(String[] commandLine) {
requireLength(commandLine, 7);
return new AmsFilamentSettingCommand(parseInt(commandLine[0]), parseInt(commandLine[1]), commandLine[2],
commandLine[3], parseInt(commandLine[4]), parseInt(commandLine[5]), commandLine[6]);
}
private AmsControlCommand parseAmsControlCommand(String[] commandLine) {
requireLength(commandLine, 1);
return AmsControlCommand.valueOf(commandLine[0]);
}
private PrintSpeedCommand parsePrintSpeedCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrintSpeedCommand.valueOf(commandLine[0]);
}
private GCodeFileCommand parseGCodeFileCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new GCodeFileCommand(commandLine[0]);
}
private GCodeLineCommand parseGCodeLineCommand(String commandLine) {
var split = commandLine.split("\n");
if (split.length < 2) {
throw new IllegalArgumentException("There are no lines for GCodeLineCommand!");
}
var lines = Arrays.stream(split).skip(1).toList();
return new GCodeLineCommand(lines, split[0]);
}
private LedControlCommand parseLedControlCommand(String[] commandLine) {
if (commandLine.length < 2) {
throw new IllegalArgumentException(
"Command line length does not match! Should be %s, but was %s!".formatted(2, commandLine.length));
}
var ledNode = LedNode.valueOf(commandLine[0]);
var ledMode = LedMode.valueOf(commandLine[1]);
@Nullable
Integer ledOnTime = null, ledOffTime = null, loopTimes = null, intervalTime = null;
if (ledMode == FLASHING) {
requireLength(commandLine, 6);
ledOnTime = parseInt(commandLine[2]);
ledOffTime = parseInt(commandLine[3]);
loopTimes = parseInt(commandLine[4]);
intervalTime = parseInt(commandLine[5]);
}
return new LedControlCommand(ledNode, ledMode, ledOnTime, ledOffTime, loopTimes, intervalTime);
}
private SystemCommand parseSystemCommand(String[] commandLine) {
requireLength(commandLine, 1);
return SystemCommand.valueOf(commandLine[0]);
}
private IpCamRecordCommand parseIpCamRecordCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new IpCamRecordCommand(parseBoolean(commandLine[0]));
}
private IpCamTimelapsCommand parseIpCamTimelapsCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new IpCamTimelapsCommand(parseBoolean(commandLine[0]));
}
private XCamControlCommand parseXCamControlCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new XCamControlCommand(XCamControlCommand.Module.valueOf(commandLine[0]), parseBoolean(commandLine[1]),
parseBoolean(commandLine[2]));
}
public static void sendCommand(@Nullable ThingActions actions, String stringCommand) {
((PrinterActions) requireNonNull(actions)).sendCommand(stringCommand);
}
@RuleAction(label = "@text/action.refreshChannels.label", description = "@text/action.refreshChannels.description")
public void refreshChannels() {
var localHandler = handler;
if (localHandler == null) {
return;
}
localHandler.refreshChannels();
}
public static void refreshChannels(@Nullable ThingActions actions) {
((PrinterActions) requireNonNull(actions)).refreshChannels();
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static pl.grzeslowski.jbambuapi.PrinterClientConfig.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PrinterConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class PrinterConfiguration {
public String serial = "";
public String scheme = SCHEME;
public String hostname = "";
public int port = DEFAULT_PORT;
public String username = LOCAL_USERNAME;
public String accessCode = "";
}

View File

@ -0,0 +1,310 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.Channel.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.Units.DECIBEL_MILLIWATTS;
import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
import static org.openhab.core.types.UnDefType.UNDEF;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.*;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedNode.*;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.PushingCommand.defaultPushingCommand;
import static pl.grzeslowski.jbambuapi.PrinterClientConfig.requiredFields;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzeslowski.jbambuapi.PrinterClient;
import pl.grzeslowski.jbambuapi.PrinterClientConfig;
import pl.grzeslowski.jbambuapi.PrinterWatcher;
import pl.grzeslowski.jbambuapi.Report;
/**
* The {@link PrinterHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.StateSubscriber {
private static final Pattern DBM_PATTERN = Pattern.compile("^(-?\\d+)dBm$");
private Logger logger = LoggerFactory.getLogger(PrinterHandler.class);
private @Nullable PrinterClient client;
public PrinterHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (!CHANNEL_LED_CHAMBER_LIGHT.getName().equals(channelUID.getId())
&& !CHANNEL_LED_WORK_LIGHT.getName().equals(channelUID.getId())) {
return;
}
var ledNode = CHANNEL_LED_CHAMBER_LIGHT.getName().equals(channelUID.getId()) ? CHAMBER_LIGHT : WORK_LIGHT;
var bambuCommand = "ON".equals(command.toFullString()) ? on(ledNode) : off(ledNode);
sendCommand(bambuCommand);
}
@Override
public void initialize() {
var config = getConfigAs(PrinterConfiguration.class);
if (config.serial.isBlank()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/printer.handler.init.noSerial");
return;
}
logger = LoggerFactory.getLogger(PrinterHandler.class.getName() + "." + config.serial);
if (config.hostname.isBlank()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/printer.handler.init.noHostname");
return;
}
var scheme = config.scheme;
var port = config.port;
var rawUri = "%s%s:%d".formatted(scheme, config.hostname, port);
URI uri;
try {
uri = new URI(rawUri);
} catch (URISyntaxException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR,
"@text/printer.handler.init.invalidHostname[\"%s\"]".formatted(rawUri));
return;
}
if (config.accessCode.isBlank()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/printer.handler.init.noAccessCode");
return;
}
if (config.username.isBlank()) {
config.username = PrinterClientConfig.LOCAL_USERNAME;
}
updateStatus(UNKNOWN);
PrinterClient localClient;
try {
localClient = client = new PrinterClient(
requiredFields(uri, config.username, config.serial, config.accessCode.toCharArray()));
} catch (Exception e) {
logger.debug("Cannot create MQTT client", e);
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
return;
}
try {
scheduler.execute(() -> {
try {
logger.debug("Trying to connect to the printer broker");
localClient.connect();
var printerWatcher = new PrinterWatcher();
localClient.subscribe(printerWatcher);
printerWatcher.subscribe(this);
// send request to update all channels
refreshChannels();
updateStatus(ONLINE);
} catch (Exception e) {
logger.debug("Cannot connect to MQTT client", e);
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
}
});
} catch (RejectedExecutionException ex) {
logger.debug("Task was rejected", ex);
updateStatus(OFFLINE, CONFIGURATION_ERROR, ex.getLocalizedMessage());
}
}
void refreshChannels() {
sendCommand(defaultPushingCommand());
}
@Override
public void dispose() {
var localClient = client;
client = null;
if (localClient != null) {
try {
localClient.close();
} catch (Exception e) {
logger.warn("Could not correctly dispose PrinterClient", e);
}
}
logger = LoggerFactory.getLogger(PrinterHandler.class);
super.dispose();
}
@Override
public void newState(@Nullable Report delta, @Nullable Report fullState) {
logger.trace("New Printer state from delta {}", delta);
// only need to update channels from delta
// do not need to use full state, because at some point in past channels was already updated with its values
if (delta == null) {
return;
}
updatePrinterChannels(delta);
// if got new Printer state (and not failed) then make sure that thing status in ONLINE
updateStatus(ONLINE);
}
private void updatePrinterChannels(Report state) {
// Print
var print = state.print();
if (print == null) {
return;
}
// tempers
updateCelsiusState(CHANNEL_NOZZLE_TEMPERATURE.getName(), print.nozzleTemper());
updateCelsiusState(CHANNEL_NOZZLE_TARGET_TEMPERATURE.getName(), print.nozzleTargetTemper());
updateCelsiusState(CHANNEL_BED_TEMPERATURE.getName(), print.bedTemper());
updateCelsiusState(CHANNEL_BED_TARGET_TEMPERATURE.getName(), print.bedTargetTemper());
updateCelsiusState(CHANNEL_CHAMBER_TEMPERATURE.getName(), print.chamberTemper());
// string
updateStringState(CHANNEL_MC_PRINT_STAGE.getName(), print.mcPrintStage());
updateStringState(CHANNEL_BED_TYPE.getName(), print.bedType());
updateStringState(CHANNEL_GCODE_FILE.getName(), print.gcodeFile());
updateStringState(CHANNEL_GCODE_STATE.getName(), print.gcodeState());
updateStringState(CHANNEL_REASON.getName(), print.reason());
updateStringState(CHANNEL_RESULT.getName(), print.result());
// percent
updatePercentState(CHANNEL_MC_PERCENT.getName(), print.mcPercent());
updatePercentState(CHANNEL_GCODE_FILE_PREPARE_PERCENT.getName(), print.gcodeFilePreparePercent());
// decimal
updateDecimalState(CHANNEL_MC_REMAINING_TIME.getName(), print.mcRemainingTime());
updateDecimalState(CHANNEL_BIG_FAN_1_SPEED.getName(), print.bigFan1Speed());
updateDecimalState(CHANNEL_BIG_FAN_2_SPEED.getName(), print.bigFan2Speed());
updateDecimalState(CHANNEL_HEAT_BREAK_FAN_SPEED.getName(), print.heatbreakFanSpeed());
updateDecimalState(CHANNEL_LAYER_NUM.getName(), print.layerNum());
updateDecimalState(CHANNEL_SPEED_LEVEL.getName(), print.spdLvl());
// boolean
updateBooleanState(CHANNEL_TIME_LAPS.getName(), print.timelapse());
updateBooleanState(CHANNEL_USE_AMS.getName(), print.useAms());
updateBooleanState(CHANNEL_VIBRATION_CALIBRATION.getName(), print.vibrationCali());
// other
if (print.wifiSignal() != null) {
updateState(CHANNEL_WIFI_SIGNAL.getName(), parseWifiChannel(print.wifiSignal()));
}
}
private void updateCelsiusState(String channelId, @Nullable Double temperature) {
if (temperature == null) {
return;
}
updateState(channelId, new QuantityType<>(temperature, CELSIUS));
}
private void updateStringState(String channelId, @Nullable String string) {
if (string == null) {
return;
}
updateState(channelId, new StringType(string));
}
private void updateDecimalState(String channelId, @Nullable Number number) {
if (number == null) {
return;
}
updateState(channelId, new DecimalType(number));
}
private void updateDecimalState(String channelId, @Nullable String number) {
if (number == null) {
return;
}
try {
var state = new DecimalType(Double.parseDouble(number));
updateState(channelId, state);
} catch (NumberFormatException e) {
logger.debug("Cannot parse decimal number {}", number, e);
updateState(channelId, UNDEF);
}
}
private void updateBooleanState(String channelId, @Nullable Boolean bool) {
if (bool == null) {
return;
}
updateState(channelId, OnOffType.from(bool));
}
private void updatePercentState(String channelId, @Nullable Integer integer) {
if (integer == null) {
return;
}
updateState(channelId, new PercentType(integer));
}
private void updatePercentState(String channelId, @Nullable String integer) {
if (integer == null) {
return;
}
try {
var state = new PercentType(integer);
updateState(channelId, state);
} catch (NumberFormatException e) {
logger.debug("Cannot parse percent number {}", integer, e);
updateState(channelId, UNDEF);
}
}
private State parseWifiChannel(String wifi) {
var matcher = DBM_PATTERN.matcher(wifi);
if (!matcher.matches()) {
return UNDEF;
}
var integer = matcher.group(1);
try {
var value = Integer.parseInt(integer);
return new QuantityType<>(value, DECIBEL_MILLIWATTS);
} catch (NumberFormatException e) {
logger.debug("Cannot parse integer {} from wifi {}", integer, wifi, e);
return UNDEF;
}
}
public void sendCommand(PrinterClient.Channel.Command command) {
logger.debug("Sending command {}", command);
var localClient = client;
if (localClient == null) {
logger.warn("Client not connected. Cannot send command {}", command);
return;
}
try {
localClient.getChannel().sendCommand(command);
} catch (Exception e) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="bambulab" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>BambuLab Binding</name>
<description>This is the binding for BambuLab.</description>
<connection>local</connection>
</addon:addon>

View File

@ -0,0 +1,210 @@
# add-on
addon.bambulab.name = BambuLab Binding
addon.bambulab.description = This is the binding for BambuLab.
# thing types
thing-type.bambulab.printer.label = Bambu Lab Printer
thing-type.bambulab.printer.description = Represents a Bambu Lab 3D printer connected via MQTT
thing-type.bambulab.printer.channel.bed-target-temperature.label = Bed Target Temperature
thing-type.bambulab.printer.channel.bed-target-temperature.description = Target temperature of the heated bed.
thing-type.bambulab.printer.channel.bed-temperature.label = Bed Temperature
thing-type.bambulab.printer.channel.bed-temperature.description = Current temperature of the heated bed.
thing-type.bambulab.printer.channel.bed-type.label = Bed Type
thing-type.bambulab.printer.channel.bed-type.description = Type of the printer's heated bed.
thing-type.bambulab.printer.channel.big-fan1-speed.label = Big Fan 1 Speed
thing-type.bambulab.printer.channel.big-fan1-speed.description = Speed of the first large cooling fan (RPM).
thing-type.bambulab.printer.channel.big-fan2-speed.label = Big Fan 2 Speed
thing-type.bambulab.printer.channel.big-fan2-speed.description = Speed of the second large cooling fan (RPM).
thing-type.bambulab.printer.channel.chamber-temperature.label = Chamber Temperature
thing-type.bambulab.printer.channel.chamber-temperature.description = Current temperature inside the printer chamber.
thing-type.bambulab.printer.channel.gcode-file.label = G-code File
thing-type.bambulab.printer.channel.gcode-file.description = Name of the currently loaded G-code file.
thing-type.bambulab.printer.channel.gcode-file-prepare-percent.label = G-code Preparation Progress
thing-type.bambulab.printer.channel.gcode-file-prepare-percent.description = Percentage of G-code file preparation completed.
thing-type.bambulab.printer.channel.gcode-state.label = G-code State
thing-type.bambulab.printer.channel.gcode-state.description = Current state of the G-code execution.
thing-type.bambulab.printer.channel.heat-break-fan-speed.label = Heat Break Fan Speed
thing-type.bambulab.printer.channel.heat-break-fan-speed.description = Speed of the heat break cooling fan (RPM).
thing-type.bambulab.printer.channel.layer-num.label = Current Layer Number
thing-type.bambulab.printer.channel.layer-num.description = Current layer being printed.
thing-type.bambulab.printer.channel.led-chamber.label = Chamber LED
thing-type.bambulab.printer.channel.led-chamber.description = Controls the LED lighting inside the printer chamber.
thing-type.bambulab.printer.channel.led-work.label = Work Area LED
thing-type.bambulab.printer.channel.led-work.description = Controls the LED lighting for the work area.
thing-type.bambulab.printer.channel.mc-percent.label = Print Progress
thing-type.bambulab.printer.channel.mc-percent.description = Percentage of the print completed.
thing-type.bambulab.printer.channel.mc-print-stage.label = Print Stage
thing-type.bambulab.printer.channel.mc-print-stage.description = Current stage of the print process.
thing-type.bambulab.printer.channel.mc-remaining-time.label = Remaining Print Time
thing-type.bambulab.printer.channel.mc-remaining-time.description = Estimated time remaining for the print in seconds.
thing-type.bambulab.printer.channel.nozzle-target-temperature.label = Nozzle Target Temperature
thing-type.bambulab.printer.channel.nozzle-target-temperature.description = Target temperature of the nozzle.
thing-type.bambulab.printer.channel.nozzle-temperature.label = Nozzle Temperature
thing-type.bambulab.printer.channel.nozzle-temperature.description = Current temperature of the nozzle.
thing-type.bambulab.printer.channel.reason.label = Pause/Stop Reason
thing-type.bambulab.printer.channel.reason.description = Reason for pausing or stopping the print.
thing-type.bambulab.printer.channel.result.label = Print Result
thing-type.bambulab.printer.channel.result.description = Final result or status of the print job.
thing-type.bambulab.printer.channel.speed-level.label = Print Speed Level
thing-type.bambulab.printer.channel.speed-level.description = Current speed setting of the print job.
thing-type.bambulab.printer.channel.time-laps.label = time-lapse Enabled
thing-type.bambulab.printer.channel.time-laps.description = Indicates whether time-lapse recording is enabled.
thing-type.bambulab.printer.channel.use-ams.label = AMS System in Use
thing-type.bambulab.printer.channel.use-ams.description = Indicates whether the Automatic Material System (AMS) is active.
thing-type.bambulab.printer.channel.vibration-calibration.label = Vibration Calibration
thing-type.bambulab.printer.channel.vibration-calibration.description = Indicates whether vibration calibration has been performed.
thing-type.bambulab.printer.channel.wifi-signal.label = WiFi Signal Strength
thing-type.bambulab.printer.channel.wifi-signal.description = Current WiFi signal strength.
# thing types config
thing-type.config.bambulab.printer.accessCode.label = Access Code
thing-type.config.bambulab.printer.accessCode.description = Access code of the printer. The method of obtaining it differs for local and cloud modes.
thing-type.config.bambulab.printer.hostname.label = Hostname
thing-type.config.bambulab.printer.hostname.description = IP address of the printer or `us.mqtt.bambulab.com` for cloud mode.
thing-type.config.bambulab.printer.port.label = Port
thing-type.config.bambulab.printer.port.description = URI port.
thing-type.config.bambulab.printer.scheme.label = Scheme
thing-type.config.bambulab.printer.scheme.description = URI scheme.
thing-type.config.bambulab.printer.serial.label = Serial Number
thing-type.config.bambulab.printer.serial.description = Unique serial number of the printer.
thing-type.config.bambulab.printer.username.label = Username
thing-type.config.bambulab.printer.username.description = `bblp` for local mode or your Bambu Lab user (starts with `u_`).
# channel types
channel-type.bambulab.boolean.label = Boolean Channel
channel-type.bambulab.number.label = Number Channel
channel-type.bambulab.on-off-command.label = ON/OFF Channel
channel-type.bambulab.on-off-command.state.option.ON = ON
channel-type.bambulab.on-off-command.state.option.OFF = OFF
channel-type.bambulab.on-off-command.command.option.ON = ON
channel-type.bambulab.on-off-command.command.option.OFF = OFF
channel-type.bambulab.percent.label = Percent Channel
channel-type.bambulab.string.label = String Channel
channel-type.bambulab.temperature-measurement.label = Current Temperature
channel-type.bambulab.temperature-setpoint.label = Target Temperature
channel-type.bambulab.wifi.label = Wi-Fi Signal Strength
channel-type.bambulab.wifi.description = Current Wi-Fi signal strength.
# channel types
channel-type.bambulab.temperature.label = Temperature
# thing types
thing-type.bambulab.printer.channel.bedTargetTemperature.label = Bed Target Temperature
thing-type.bambulab.printer.channel.bedTargetTemperature.description = Target temperature of the heated bed.
thing-type.bambulab.printer.channel.bedTemperature.label = Bed Temperature
thing-type.bambulab.printer.channel.bedTemperature.description = Current temperature of the heated bed.
thing-type.bambulab.printer.channel.bedType.label = Bed Type
thing-type.bambulab.printer.channel.bedType.description = Type of the printer's heated bed.
thing-type.bambulab.printer.channel.bigFan1Speed.label = Big Fan 1 Speed
thing-type.bambulab.printer.channel.bigFan1Speed.description = Speed of the first large cooling fan (RPM).
thing-type.bambulab.printer.channel.bigFan2Speed.label = Big Fan 2 Speed
thing-type.bambulab.printer.channel.bigFan2Speed.description = Speed of the second large cooling fan (RPM).
thing-type.bambulab.printer.channel.chamberTemperature.label = Chamber Temperature
thing-type.bambulab.printer.channel.chamberTemperature.description = Current temperature inside the printer chamber.
thing-type.bambulab.printer.channel.gcodeFile.label = G-code File
thing-type.bambulab.printer.channel.gcodeFile.description = Name of the currently loaded G-code file.
thing-type.bambulab.printer.channel.gcodeFilePreparePercent.label = G-code Preparation Progress
thing-type.bambulab.printer.channel.gcodeFilePreparePercent.description = Percentage of G-code file preparation completed.
thing-type.bambulab.printer.channel.gcodeState.label = G-code State
thing-type.bambulab.printer.channel.gcodeState.description = Current state of the G-code execution.
thing-type.bambulab.printer.channel.heatBreakFanSpeed.label = Heat Break Fan Speed
thing-type.bambulab.printer.channel.heatBreakFanSpeed.description = Speed of the heat break cooling fan (RPM).
thing-type.bambulab.printer.channel.layerNum.label = Current Layer Number
thing-type.bambulab.printer.channel.layerNum.description = Current layer being printed.
thing-type.bambulab.printer.channel.ledChamber.label = Chamber LED
thing-type.bambulab.printer.channel.ledChamber.description = Controls the LED lighting inside the printer chamber.
thing-type.bambulab.printer.channel.ledWork.label = Work Area LED
thing-type.bambulab.printer.channel.ledWork.description = Controls the LED lighting for the work area.
thing-type.bambulab.printer.channel.mcPercent.label = Print Progress
thing-type.bambulab.printer.channel.mcPercent.description = Percentage of the print completed.
thing-type.bambulab.printer.channel.mcPrintStage.label = Print Stage
thing-type.bambulab.printer.channel.mcPrintStage.description = Current stage of the print process.
thing-type.bambulab.printer.channel.mcRemainingTime.label = Remaining Print Time
thing-type.bambulab.printer.channel.mcRemainingTime.description = Estimated time remaining for the print in seconds.
thing-type.bambulab.printer.channel.nozzleTargetTemperature.label = Nozzle Target Temperature
thing-type.bambulab.printer.channel.nozzleTargetTemperature.description = Target temperature of the nozzle.
thing-type.bambulab.printer.channel.nozzleTemperature.label = Nozzle Temperature
thing-type.bambulab.printer.channel.nozzleTemperature.description = Current temperature of the nozzle.
thing-type.bambulab.printer.channel.speedLevel.label = Print Speed Level
thing-type.bambulab.printer.channel.speedLevel.description = Current speed setting of the print job.
thing-type.bambulab.printer.channel.timeLaps.label = Timelapse Enabled
thing-type.bambulab.printer.channel.timeLaps.description = Indicates whether timelapse recording is enabled.
thing-type.bambulab.printer.channel.useAms.label = AMS System in Use
thing-type.bambulab.printer.channel.useAms.description = Indicates whether the Automatic Material System (AMS) is active.
thing-type.bambulab.printer.channel.vibrationCalibration.label = Vibration Calibration
thing-type.bambulab.printer.channel.vibrationCalibration.description = Indicates whether vibration calibration has been performed.
thing-type.bambulab.printer.channel.wifiSignal.label = WiFi Signal Strength
thing-type.bambulab.printer.channel.wifiSignal.description = Current WiFi signal strength.
# channel types
channel-type.bambulab.boolean-channel.label = Boolean Channel
channel-type.bambulab.number-channel.label = Number Channel
channel-type.bambulab.on-off-command-channel.label = ON/OFF Channel
channel-type.bambulab.on-off-command-channel.state.option.ON = ON
channel-type.bambulab.on-off-command-channel.state.option.OFF = OFF
channel-type.bambulab.on-off-command-channel.command.option.ON = ON
channel-type.bambulab.on-off-command-channel.command.option.OFF = OFF
channel-type.bambulab.percent-channel.label = Percent Channel
channel-type.bambulab.string-channel.label = String Channel
channel-type.bambulab.temperature-channel.label = Temperature
channel-type.bambulab.wifi-channel.label = WiFi Signal Strength
channel-type.bambulab.wifi-channel.description = Current WiFi signal strength.
# thing types
thing-type.bambulab.printer.channel.command.label = Printer Command
thing-type.bambulab.printer.channel.command.description = Current command being executed by the printer.
thing-type.bambulab.printer.channel.message.label = Message Code
thing-type.bambulab.printer.channel.message.description = Message code from the printer.
thing-type.bambulab.printer.channel.sequenceId.label = Sequence ID
thing-type.bambulab.printer.channel.sequenceId.description = Unique sequence identifier for commands.
thing-type.bambulab.printer.channel.buildplateMarkerDetector.label = Buildplate Marker Detector
thing-type.bambulab.printer.channel.buildplateMarkerDetector.description = Indicates if the buildplate marker detection is enabled.
thing-type.bambulab.printer.channel.ipcam.label = IP Camera
thing-type.bambulab.printer.channel.ipcam.description = IP camera details of the printer.
thing-type.bambulab.printer.channel.ipcamDev.label = IP Camera Device
thing-type.bambulab.printer.channel.ipcamDev.description = IP camera device associated with the printer.
thing-type.bambulab.printer.channel.ipcamRecord.label = IP Camera Record
thing-type.bambulab.printer.channel.ipcamRecord.description = Current recording status of the IP camera.
thing-type.bambulab.printer.channel.modeBits.label = Mode Bits
thing-type.bambulab.printer.channel.modeBits.description = Mode bits setting of the IP camera.
thing-type.bambulab.printer.channel.net.label = Network
thing-type.bambulab.printer.channel.net.description = Network configuration and info.
thing-type.bambulab.printer.channel.netConf.label = Network Configuration
thing-type.bambulab.printer.channel.netConf.description = Configuration settings of the printer network.
thing-type.bambulab.printer.channel.resolution.label = Camera Resolution
thing-type.bambulab.printer.channel.resolution.description = Resolution of the IP camera.
thing-type.bambulab.printer.channel.timelapse.label = Timelapse
thing-type.bambulab.printer.channel.timelapse.description = Status of the timelapse recording.
thing-type.bambulab.printer.channel.tutkServer.label = Tutk Server
thing-type.bambulab.printer.channel.tutkServer.description = Tutk server details for IP camera.
thing-type.bambulab.printer.channel.upgradeState.label = Upgrade State
thing-type.bambulab.printer.channel.upgradeState.description = Current state of the printer firmware upgrade.
thing-type.bambulab.printer.channel.upload.label = Upload
thing-type.bambulab.printer.channel.upload.description = Upload status details.
thing-type.bambulab.printer.channel.uploadMessage.label = Upload Message
thing-type.bambulab.printer.channel.uploadMessage.description = Message related to the upload status.
thing-type.bambulab.printer.channel.uploadProgress.label = Upload Progress
thing-type.bambulab.printer.channel.uploadProgress.description = File upload progress percentage.
thing-type.bambulab.printer.channel.uploadStatus.label = Upload Status
thing-type.bambulab.printer.channel.uploadStatus.description = Current file upload status.
thing-type.bambulab.printer.channel.xcam.label = XCam
thing-type.bambulab.printer.channel.xcam.description = XCam details of the printer.
# other
action.sendCommand.label = Sends command to 3D printer
action.sendCommand.description = Transmits a command to the 3D printer for execution, enabling direct control over its operations.
action.refreshChannels.label = Reports the complete status of the printer
action.refreshChannels.description = This is unnecessary for the X1 series since it already transmits the full object each time. However, the P1 series only sends the values that have been updated compared to the previous report. As a rule of thumb, refrain from executing this command at intervals less than 5 minutes on the P1P, as it may cause lag due to its hardware limitations.
printer.handler.init.noHostname = Please pass hostname!
printer.handler.init.invalidHostname = Invalid hostname "{0}"!
printer.handler.init.noSerial = Please pass serial!
printer.handler.init.noAccessCode = Please pass access code!

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bambulab"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="temperature-measurement">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="temperature-setpoint">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<category>Temperature</category>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="string">
<item-type>String</item-type>
<label>String Channel</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="wifi">
<item-type unitHint="dBm">Number:Power</item-type>
<label>Wi-Fi Signal Strength</label>
<description>Current Wi-Fi signal strength.</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="number">
<item-type>Number</item-type>
<label>Number Channel</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="percent">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Percent Channel</label>
<state readOnly="true" min="0" max="100" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="boolean">
<item-type>Switch</item-type>
<label>Boolean Channel</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="on-off-command">
<item-type>String</item-type>
<label>ON/OFF Channel</label>
<category>Lightbulb</category>
<state>
<options>
<option value="ON">ON</option>
<option value="OFF">OFF</option>
</options>
</state>
<command>
<options>
<option value="ON">ON</option>
<option value="OFF">OFF</option>
</options>
</command>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bambulab"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="printer">
<label>Bambu Lab Printer</label>
<description>Represents a Bambu Lab 3D printer connected via MQTT</description>
<channels>
<channel id="nozzle-temperature" typeId="temperature-measurement">
<label>Nozzle Temperature</label>
<description>Current temperature of the nozzle.</description>
</channel>
<channel id="nozzle-target-temperature" typeId="temperature-setpoint">
<label>Nozzle Target Temperature</label>
<description>Target temperature of the nozzle.</description>
</channel>
<channel id="bed-temperature" typeId="temperature-measurement">
<label>Bed Temperature</label>
<description>Current temperature of the heated bed.</description>
</channel>
<channel id="bed-target-temperature" typeId="temperature-setpoint">
<label>Bed Target Temperature</label>
<description>Target temperature of the heated bed.</description>
</channel>
<channel id="chamber-temperature" typeId="temperature-measurement">
<label>Chamber Temperature</label>
<description>Current temperature inside the printer chamber.</description>
</channel>
<channel id="mc-print-stage" typeId="string">
<label>Print Stage</label>
<description>Current stage of the print process.</description>
</channel>
<channel id="mc-percent" typeId="percent">
<label>Print Progress</label>
<description>Percentage of the print completed.</description>
</channel>
<channel id="mc-remaining-time" typeId="number">
<label>Remaining Print Time</label>
<description>Estimated time remaining for the print in seconds.</description>
</channel>
<channel id="wifi-signal" typeId="wifi">
<label>WiFi Signal Strength</label>
<description>Current WiFi signal strength.</description>
</channel>
<channel id="bed-type" typeId="string">
<label>Bed Type</label>
<description>Type of the printer's heated bed.</description>
</channel>
<channel id="gcode-file" typeId="string">
<label>G-code File</label>
<description>Name of the currently loaded G-code file.</description>
</channel>
<channel id="gcode-state" typeId="string">
<label>G-code State</label>
<description>Current state of the G-code execution.</description>
</channel>
<channel id="reason" typeId="string">
<label>Pause/Stop Reason</label>
<description>Reason for pausing or stopping the print.</description>
</channel>
<channel id="result" typeId="string">
<label>Print Result</label>
<description>Final result or status of the print job.</description>
</channel>
<channel id="gcode-file-prepare-percent" typeId="percent">
<label>G-code Preparation Progress</label>
<description>Percentage of G-code file preparation completed.</description>
</channel>
<channel id="big-fan1-speed" typeId="number">
<label>Big Fan 1 Speed</label>
<description>Speed of the first large cooling fan (RPM).</description>
</channel>
<channel id="big-fan2-speed" typeId="number">
<label>Big Fan 2 Speed</label>
<description>Speed of the second large cooling fan (RPM).</description>
</channel>
<channel id="heat-break-fan-speed" typeId="number">
<label>Heat Break Fan Speed</label>
<description>Speed of the heat break cooling fan (RPM).</description>
</channel>
<channel id="layer-num" typeId="number">
<label>Current Layer Number</label>
<description>Current layer being printed.</description>
</channel>
<channel id="speed-level" typeId="number">
<label>Print Speed Level</label>
<description>Current speed setting of the print job.</description>
</channel>
<channel id="time-laps" typeId="boolean">
<label>time-lapse Enabled</label>
<description>Indicates whether time-lapse recording is enabled.</description>
</channel>
<channel id="use-ams" typeId="boolean">
<label>AMS System in Use</label>
<description>Indicates whether the Automatic Material System (AMS) is active.</description>
</channel>
<channel id="vibration-calibration" typeId="boolean">
<label>Vibration Calibration</label>
<description>Indicates whether vibration calibration has been performed.</description>
</channel>
<!-- command channels -->
<channel id="led-chamber" typeId="on-off-command">
<label>Chamber LED</label>
<description>Controls the LED lighting inside the printer chamber.</description>
</channel>
<channel id="led-work" typeId="on-off-command">
<label>Work Area LED</label>
<description>Controls the LED lighting for the work area.</description>
</channel>
</channels>
<representation-property>serial</representation-property>
<config-description>
<parameter name="serial" type="text" required="true">
<label>Serial Number</label>
<description>Unique serial number of the printer.</description>
</parameter>
<parameter name="scheme" type="text" required="false">
<label>Scheme</label>
<description>URI scheme.</description>
<advanced>true</advanced>
</parameter>
<parameter name="hostname" type="text" required="true">
<label>Hostname</label>
<description>IP address of the printer or `us.mqtt.bambulab.com` for cloud mode.</description>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer" required="false">
<label>Port</label>
<description>URI port.</description>
<advanced>true</advanced>
</parameter>
<parameter name="username" type="text">
<label>Username</label>
<description>`bblp` for local mode or your Bambu Lab user (starts with `u_`).</description>
<advanced>true</advanced>
</parameter>
<parameter name="accessCode" type="text" required="true">
<label>Access Code</label>
<description>Access code of the printer. The method of obtaining it differs for local and cloud modes.
</description>
<context>password</context>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,176 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedMode.*;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedNode.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.assertj.core.api.ThrowableAssert;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import pl.grzeslowski.jbambuapi.PrinterClient;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsControlCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsFilamentSettingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.AmsUserSettingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.ChangeFilamentCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.Command;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.InfoCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.IpCamRecordCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.PrintSpeedCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.PushingCommand;
import pl.grzeslowski.jbambuapi.PrinterClient.Channel.SystemCommand;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
class PrinterActionsTest {
PrinterActions printerActions = new PrinterActions();
@Mock
@Nullable
PrinterHandler printerHandler;
@BeforeEach
void setUp() {
printerActions.setThingHandler(requireNonNull(printerHandler));
}
@Test
@DisplayName("should correctly parse GCodeLineCommand")
void gcodeLines() {
// given
var command = """
GCodeLine:123
G28 ; Home all axes
G90 ; Set to absolute positioning
G1 X50 Y50 Z10 F1500 ; Move to position (50,50,10) at 1500 mm/min
G1 X100 Y100 Z20 F2000 ; Move to position (100,100,20) at 2000 mm/min
M104 S200 ; Set hotend temperature to 200°C""";
// when
printerActions.sendCommand(command);
// then
verify(requireNonNull(printerHandler)).sendCommand(new PrinterClient.Channel.GCodeLineCommand(
List.of("G28 ; Home all axes", "G90 ; Set to absolute positioning",
"G1 X50 Y50 Z10 F1500 ; Move to position (50,50,10) at 1500 mm/min",
"G1 X100 Y100 Z20 F2000 ; Move to position (100,100,20) at 2000 mm/min",
"M104 S200 ; Set hotend temperature to 200°C"),
"123"));
}
@Test
@DisplayName("should throw exception if there are no lines for GCodeLineCommand")
void gcodeLineNoLines() {
// given
var command = "GCodeLine:123";
// when
ThrowableAssert.ThrowingCallable when = () -> printerActions.sendCommand(command);
// then
assertThatThrownBy(when)//
.isInstanceOf(IllegalArgumentException.class)//
.hasMessage("There are no lines for GCodeLineCommand!");
}
@ParameterizedTest(name = "{index}: should {0}")
@MethodSource
void shouldRunCommand(String command, Command expectedCommand) {
// when
printerActions.sendCommand(command);
// then
verify(requireNonNull(printerHandler)).sendCommand(expectedCommand);
}
static Stream<Arguments> shouldRunCommand() {
var infoCommandStream = Arrays.stream(InfoCommand.values())//
.map(value -> Arguments.of("Info:" + value.name(), value));
var pushingCommandStream = stream(Arguments.of("Pushing:11:22", new PushingCommand(11, 22)));
var printCommandStream = Arrays.stream(PrinterClient.Channel.PrintCommand.values())//
.map(value -> Arguments.of("Print:" + value.name(), value));
var changeFilamentCommandStream = stream(
Arguments.of("ChangeFilament:11:22:33", new ChangeFilamentCommand(11, 22, 33)));
var amsUserSettingCommandStream = stream(
Arguments.of("AmsUserSetting:11:tRuE:FaLsE", new AmsUserSettingCommand(11, true, false)));
var amsFilamentSettingCommandStream = stream(Arguments.of("AmsFilamentSetting:11:22:s3:s4:55:66:s7",
new AmsFilamentSettingCommand(11, 22, "s3", "s4", 55, 66, "s7")));
var amsControlCommandStream = Arrays.stream(AmsControlCommand.values())//
.map(value -> Arguments.of("AmsControl:" + value.name(), value));
var printSpeedCommandStream = Arrays.stream(PrintSpeedCommand.values())//
.map(value -> Arguments.of("PrintSpeed:" + value.name(), value));
var gCodeFileCommandStream = stream(
Arguments.of("GCodeFile:s1", new PrinterClient.Channel.GCodeFileCommand("s1")));
var gCodeLineCommandStream = stream(Arguments.of("""
GCodeLine:s1
l1
l2
l3""", new PrinterClient.Channel.GCodeLineCommand(List.of("l1", "l2", "l3"), "s1")));
var ledControlCommandStream = stream(//
Arguments.of("LedControl:CHAMBER_LIGHT:ON",
new LedControlCommand(CHAMBER_LIGHT, ON, null, null, null, null)), //
Arguments.of("LedControl:WORK_LIGHT:OFF",
new LedControlCommand(WORK_LIGHT, OFF, null, null, null, null)), //
Arguments.of("LedControl:CHAMBER_LIGHT:FLASHING:11:22:33:44",
new LedControlCommand(CHAMBER_LIGHT, FLASHING, 11, 22, 33, 44))//
);
var systemCommandStream = Arrays.stream(SystemCommand.values())//
.map(value -> Arguments.of("System:" + value.name(), value));
var ipCamRecordCommandStream = stream(//
Arguments.of("IpCamRecord:tRue", new IpCamRecordCommand(true)), //
Arguments.of("IpCamRecord:fAlSe", new IpCamRecordCommand(false))//
);
var ipCamTimelapsCommandStream = stream(//
Arguments.of("IpCamTimelaps:tRue", new PrinterClient.Channel.IpCamTimelapsCommand(true)), //
Arguments.of("IpCamTimelaps:fAlSe", new PrinterClient.Channel.IpCamTimelapsCommand(false))//
);
var xCamControlCommandStream = Arrays.stream(PrinterClient.Channel.XCamControlCommand.Module.values())//
.map(moduleValue -> Arguments.of("XCamControl:%s:trUE:FAlse".formatted(moduleValue),
new PrinterClient.Channel.XCamControlCommand(moduleValue, true, false)));
return concat(infoCommandStream, pushingCommandStream, printCommandStream, changeFilamentCommandStream,
amsUserSettingCommandStream, amsFilamentSettingCommandStream, amsControlCommandStream,
printSpeedCommandStream, gCodeFileCommandStream, gCodeLineCommandStream, ledControlCommandStream,
systemCommandStream, ipCamRecordCommandStream, ipCamTimelapsCommandStream, xCamControlCommandStream);
}
@SafeVarargs
static Stream<Arguments> concat(Stream<Arguments>... streams) {
return Stream.of(streams).flatMap(s -> s);
}
@SafeVarargs
static <T> Stream<T> stream(T... values) {
return Stream.of(values);
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bambulab.internal;
import static java.util.Arrays.stream;
import static java.util.function.Predicate.not;
import static org.mockito.Mockito.*;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.Channel.*;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.*;
import static pl.grzeslowski.jbambuapi.PrinterClient.Channel.LedControlCommand.LedNode.*;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.bambulab.internal.BambuLabBindingConstants.Channel;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import pl.grzeslowski.jbambuapi.PrinterClient;
/**
* @author Martin Grześlowski - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
class PrinterHandlerTest {
@ParameterizedTest(name = "Should handle {0} command for {1} channel and send {2}")
@MethodSource
public void testSendLightCommand(OnOffType command, Channel channel, PrinterClient.Channel.Command sendCommand) {
// Given
var printerHandler = spy(new PrinterHandler(mock(Thing.class)));
var channelUID = new ChannelUID("bambulab:printer:test:" + channel);
doNothing().when(printerHandler).sendCommand(any(PrinterClient.Channel.Command.class));
// When
printerHandler.handleCommand(channelUID, command);
// Then
verify(printerHandler).sendCommand(eq(sendCommand));
}
static Stream<Arguments> testSendLightCommand() {
return Stream.of(//
Arguments.of(OnOffType.ON, CHANNEL_LED_CHAMBER_LIGHT, on(CHAMBER_LIGHT)), //
Arguments.of(OnOffType.OFF, CHANNEL_LED_CHAMBER_LIGHT, off(CHAMBER_LIGHT)), //
Arguments.of(OnOffType.ON, CHANNEL_LED_WORK_LIGHT, on(WORK_LIGHT)), //
Arguments.of(OnOffType.OFF, CHANNEL_LED_WORK_LIGHT, off(WORK_LIGHT)));
}
@ParameterizedTest(name = "Command to channel {0} should not invoke `client.sendCommand`")
@MethodSource
public void notImplementedCommands(Channel channel) {
// Given
var printerHandler = spy(new PrinterHandler(mock(Thing.class)));
var channelUID = new ChannelUID("bambulab:printer:test:" + channel);
// When
printerHandler.handleCommand(channelUID, OnOffType.ON);
// Then
verify(printerHandler, never()).sendCommand(any());
}
static Stream<Arguments> notImplementedCommands() {
return stream(Channel.values())//
.filter(not(Channel::isSupportCommand))//
.map(Arguments::of);
}
}

View File

@ -94,11 +94,11 @@ public class PiHoleHandler extends BaseThingHandler implements AdminService {
hostname = new URI(config.hostname);
} catch (URISyntaxException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR,
"@token/handler.init.invalidHostname[\"" + config.hostname + "\"]");
"@text/handler.init.invalidHostname[\"" + config.hostname + "\"]");
return;
}
if (config.token.isEmpty()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@token/handler.init.noToken");
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.noToken");
return;
}
adminService = new JettyAdminService(config.token, hostname, httpClient);

View File

@ -71,6 +71,7 @@
<module>org.openhab.binding.automower</module>
<module>org.openhab.binding.avmfritz</module>
<module>org.openhab.binding.awattar</module>
<module>org.openhab.binding.bambulab</module>
<module>org.openhab.binding.benqprojector</module>
<module>org.openhab.binding.bigassfan</module>
<module>org.openhab.binding.bluetooth</module>