From 9ac731580e994fb6fea477217b7ea1a032a4bd22 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 12 Mar 2025 22:14:31 +0100 Subject: [PATCH] [bambulab] Initial contribution (#18369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Init BambuLab Signed-off-by: Martin Grześlowski --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.bambulab/NOTICE | 13 + .../org.openhab.binding.bambulab/README.md | 172 ++++++++++ bundles/org.openhab.binding.bambulab/pom.xml | 54 +++ .../src/main/feature/feature.xml | 9 + .../internal/BambuLabBindingConstants.java | 85 +++++ .../internal/BambuLabHandlerFactory.java | 55 ++++ .../bambulab/internal/PrinterActions.java | 266 +++++++++++++++ .../internal/PrinterConfiguration.java | 32 ++ .../bambulab/internal/PrinterHandler.java | 310 ++++++++++++++++++ .../src/main/resources/OH-INF/addon/addon.xml | 10 + .../resources/OH-INF/i18n/bambulab.properties | 210 ++++++++++++ .../resources/OH-INF/thing/channel-types.xml | 70 ++++ .../resources/OH-INF/thing/thing-types.xml | 156 +++++++++ .../bambulab/internal/PrinterActionsTest.java | 176 ++++++++++ .../bambulab/internal/PrinterHandlerTest.java | 85 +++++ .../pihole/internal/PiHoleHandler.java | 4 +- bundles/pom.xml | 1 + 19 files changed, 1712 insertions(+), 2 deletions(-) create mode 100644 bundles/org.openhab.binding.bambulab/NOTICE create mode 100644 bundles/org.openhab.binding.bambulab/README.md create mode 100644 bundles/org.openhab.binding.bambulab/pom.xml create mode 100644 bundles/org.openhab.binding.bambulab/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java create mode 100644 bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java create mode 100644 bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java create mode 100644 bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java create mode 100644 bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java create mode 100644 bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties create mode 100644 bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml create mode 100644 bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java create mode 100644 bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 070ae182f12..d35aff9f7fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index ba3661159c5..b7ecf76dcac 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -181,6 +181,11 @@ org.openhab.binding.awattar ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.bambulab + ${project.version} + org.openhab.addons.bundles org.openhab.binding.benqprojector diff --git a/bundles/org.openhab.binding.bambulab/NOTICE b/bundles/org.openhab.binding.bambulab/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.bambulab/README.md b/bundles/org.openhab.binding.bambulab/README.md new file mode 100644 index 00000000000..045294c4dec --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/README.md @@ -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. | diff --git a/bundles/org.openhab.binding.bambulab/pom.xml b/bundles/org.openhab.binding.bambulab/pom.xml new file mode 100644 index 00000000000..bf7d7be4e5f --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.0.0-SNAPSHOT + + + org.openhab.binding.bambulab + + openHAB Add-ons :: Bundles :: BambuLab Binding + + + + pl.grzeslowski + JBambuAPI + 1.0.0 + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + org.slf4j + slf4j-api + 2.0.16 + + + + + org.assertj + assertj-core + 3.25.3 + test + + + org.mockito + mockito-core + 5.11.0 + test + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/feature/feature.xml b/bundles/org.openhab.binding.bambulab/src/main/feature/feature.xml new file mode 100644 index 00000000000..042189544d8 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.bambulab/${project.version} + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java new file mode 100644 index 00000000000..32daad84314 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java @@ -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; + } + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java new file mode 100644 index 00000000000..62f05f625ca --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java new file mode 100644 index 00000000000..aafd23f1565 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java new file mode 100644 index 00000000000..f8640ee8c5e --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java @@ -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 = ""; +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java new file mode 100644 index 00000000000..26d7fd9f07d --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java @@ -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()); + } + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..6344f0234eb --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + binding + BambuLab Binding + This is the binding for BambuLab. + local + diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties new file mode 100644 index 00000000000..ed3c24d1308 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties @@ -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! diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 00000000000..9a25f9d8172 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,70 @@ + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + + + + Number:Temperature + + Temperature + + Setpoint + Temperature + + + + + String + + + + + Number:Power + + Current Wi-Fi signal strength. + + + + Number + + + + + Number:Dimensionless + + + + + Switch + + + + + + String + + Lightbulb + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..b43a4706d31 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,156 @@ + + + + + + Represents a Bambu Lab 3D printer connected via MQTT + + + + + Current temperature of the nozzle. + + + + Target temperature of the nozzle. + + + + Current temperature of the heated bed. + + + + Target temperature of the heated bed. + + + + Current temperature inside the printer chamber. + + + + Current stage of the print process. + + + + Percentage of the print completed. + + + + Estimated time remaining for the print in seconds. + + + + Current WiFi signal strength. + + + + Type of the printer's heated bed. + + + + Name of the currently loaded G-code file. + + + + Current state of the G-code execution. + + + + Reason for pausing or stopping the print. + + + + Final result or status of the print job. + + + + Percentage of G-code file preparation completed. + + + + Speed of the first large cooling fan (RPM). + + + + Speed of the second large cooling fan (RPM). + + + + Speed of the heat break cooling fan (RPM). + + + + Current layer being printed. + + + + Current speed setting of the print job. + + + + Indicates whether time-lapse recording is enabled. + + + + Indicates whether the Automatic Material System (AMS) is active. + + + + Indicates whether vibration calibration has been performed. + + + + + + Controls the LED lighting inside the printer chamber. + + + + Controls the LED lighting for the work area. + + + + serial + + + + Unique serial number of the printer. + + + + + URI scheme. + true + + + + + IP address of the printer or `us.mqtt.bambulab.com` for cloud mode. + network-address + + + + + URI port. + true + + + + + `bblp` for local mode or your Bambu Lab user (starts with `u_`). + true + + + + + Access code of the printer. The method of obtaining it differs for local and cloud modes. + + password + + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java new file mode 100644 index 00000000000..345489d9f81 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java @@ -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 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 concat(Stream... streams) { + return Stream.of(streams).flatMap(s -> s); + } + + @SafeVarargs + static Stream stream(T... values) { + return Stream.of(values); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java new file mode 100644 index 00000000000..9c6de00449c --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java @@ -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 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 notImplementedCommands() { + return stream(Channel.values())// + .filter(not(Channel::isSupportCommand))// + .map(Arguments::of); + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java index d082ad28bd6..f2f790ca8a1 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java @@ -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); diff --git a/bundles/pom.xml b/bundles/pom.xml index a9f84c0b1a0..61af0ec2a95 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -71,6 +71,7 @@ org.openhab.binding.automower org.openhab.binding.avmfritz org.openhab.binding.awattar + org.openhab.binding.bambulab org.openhab.binding.benqprojector org.openhab.binding.bigassfan org.openhab.binding.bluetooth