[bambulab] Support AMS (#18448)

* camera

Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
Martin
2025-03-28 15:41:58 +01:00
committed by GitHub
parent 5537062b1a
commit 5602458252
23 changed files with 3047 additions and 411 deletions
+33 -30
View File
@@ -65,6 +65,8 @@ Use `us.mqtt.bambulab.com` as the hostname.
| `hostname` | N/A | Text | Yes | IP address of the printer or `us.mqtt.bambulab.com` for cloud mode. |
| `accessCode` | N/A | Text | Yes | Access code for the printer. The method of obtaining this varies between local and cloud modes. |
| `username` | N/A | Text | No | `bblp` for local mode or your Bambu Lab user (starting with `u_`). (Advanced) |
| `reconnectTime` | MQTT | Integer | No | Time in seconds to wait before reconnect. (Advanced) |
| `reconnectMax` | MQTT | Integer | No | Max times to try reconnections. (Advanced) |
| `scheme` | MQTT | Text | No | URI scheme. (Advanced) |
| `port` | MQTT | Integer | No | URI port. (Advanced) |
| `cameraPort` | Camera | Integer | No | Camera server port. (Advanced) |
@@ -72,35 +74,36 @@ Use `us.mqtt.bambulab.com` as the hostname.
## 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. |
| `camera-record` | Boolean RW Command | Turns on/off OpenHAB's capability of recording. |
| `camera-image` | Image | Current image from the printer's camera. |
| Channel ID | Type | Description |
|------------------------------|---------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `command` | String RW Channel | Send yor commands to this channel so they will be send to the printer. Check `Actions` > `sendCommand` for detailed documentation. |
| `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. |
| `camera-record` | Boolean RW Command | Turns on/off OpenHAB's capability of recording. |
| `camera-image` | Image | Current image from the printer's camera. |
## Full Example
@@ -165,7 +168,7 @@ CommandType:Parameter1:Parameter2:...
| `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. |
| `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 |
+8 -1
View File
@@ -18,7 +18,7 @@
<dependency>
<groupId>pl.grzeslowski</groupId>
<artifactId>JBambuAPI</artifactId>
<version>1.1.0</version>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
@@ -34,6 +34,7 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
<scope>provided</scope>
</dependency>
<!-- TEST -->
@@ -49,6 +50,12 @@
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,33 @@
/*
* 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.AmsChannel.*;
import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class AmsDeviceConfiguration {
public int number = -1;
public void validateNumber() throws InitializationException {
if (number < MIN_AMS || number > MAX_AMS) {
var message = "AMS number has to be between %s and %s!".formatted(MIN_AMS, MAX_AMS);
throw new InitializationException(CONFIGURATION_ERROR, message);
}
}
}
@@ -0,0 +1,228 @@
/*
* 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.openhab.binding.bambulab.internal.BambuLabBindingConstants.AmsChannel.MAX_AMS_TRAYS;
import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatusDetail.BRIDGE_UNINITIALIZED;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bambulab.internal.BambuLabBindingConstants.AmsChannel;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
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;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class AmsDeviceHandlerFactory extends BaseThingHandler {
private Logger logger = LoggerFactory.getLogger(AmsDeviceHandlerFactory.class);
private @Nullable AmsDeviceConfiguration config;
public AmsDeviceHandlerFactory(Thing thing) {
super(thing);
}
@Override
public void initialize() {
try {
internalInitialize();
} catch (InitializationException e) {
logger.debug("Error during initialization", e);
updateStatus(OFFLINE, e.getThingStatusDetail(), e.getDescription());
}
}
private void internalInitialize() throws InitializationException {
var printer = validateBridge();
var config = this.config = getConfigAs(AmsDeviceConfiguration.class);
config.validateNumber();
logger = LoggerFactory.getLogger("%s.%s.%d".formatted(AmsDeviceHandlerFactory.class.getName(),
printer.getSerialNumber(), config.number));
updateStatus(ONLINE);
}
private PrinterHandler validateBridge() throws InitializationException {
var bridge = getBridge();
if (bridge == null) {
throw new InitializationException(BRIDGE_UNINITIALIZED,
"@text/thing-type.config.bambulab.ams-device.init.no-bridge");
}
if (!(bridge.getHandler() instanceof PrinterHandler printer)) {
throw new InitializationException(BRIDGE_UNINITIALIZED,
"@text/thing-type.config.bambulab.ams-device.init.bridge-wrong-type");
}
return printer;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no commands to handle
}
public void updateAms(Map<String, Object> ams) {
if (logger.isDebugEnabled()) {
var number = Optional.ofNullable(config)//
.map(c -> c.number)//
.map(Objects::toString)//
.orElse("?!");
logger.debug("Updating AMS #{}", number);
}
Optional.of(ams)//
.map(map -> map.get("tray"))//
.filter(obj -> obj instanceof Collection<?>)//
.map(obj -> (Collection<?>) obj)//
.stream()//
.flatMap(Collection::stream)//
.filter(obj -> obj instanceof Map<?, ?>)//
.map(obj -> (Map<?, ?>) obj)//
.forEach(this::updateAmsTray);
}
private void updateAmsTray(Map<?, ?> map) {
var someId = findKey(map, "id")//
.map(Object::toString)//
.map(Integer::parseInt)
// tray ID in api starts from 0 and for channels it starts for 1
.map(t -> t + 1);
if (someId.isEmpty()) {
logger.warn("There is no tray ID in {}", map);
return;
}
int trayId = someId.get();
if (trayId > MAX_AMS_TRAYS) {
logger.warn("Tray ID needs to be lower that {}. Was {}", MAX_AMS_TRAYS, trayId);
return;
}
findKey(map, "tray_type")//
.map(Object::toString)//
.flatMap(AmsChannel.TrayType::findTrayType)//
.map(Enum::name)//
.map(value -> (State) StringType.valueOf(value))//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayTypeChannel(trayId), value));
findKey(map, "tray_color")//
.map(Object::toString)//
.map(StateParserHelper::parseColor)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayColorChannel(trayId), value));
findKey(map, "nozzle_temp_max")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseTemperatureType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getNozzleTemperatureMaxChannel(trayId), value));
findKey(map, "nozzle_temp_min")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseTemperatureType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getNozzleTemperatureMinChannel(trayId), value));
findKey(map, "remain")//
.map(Object::toString)//
.flatMap(StateParserHelper::parsePercentType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getRemainChannel(trayId), value));
findKey(map, "k")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getKChannel(trayId), value));
findKey(map, "n")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getNChannel(trayId), value));
findKey(map, "tag_uuid")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTagUuidChannel(trayId), value));
findKey(map, "tray_id_name")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayIdNameChannel(trayId), value));
findKey(map, "tray_info_idx")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayInfoIdxChannel(trayId), value));
findKey(map, "tray_sub_brands")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTraySubBrandsChannel(trayId), value));
findKey(map, "tray_weight")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayWeightChannel(trayId), value));
findKey(map, "tray_diameter")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayDiameterChannel(trayId), value));
findKey(map, "tray_temp")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseTemperatureType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayTemperatureChannel(trayId), value));
findKey(map, "tray_time")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getTrayTimeChannel(trayId), value));
findKey(map, "bed_temp_type")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getBedTemperatureTypeChannel(trayId), value));
findKey(map, "bed_temp")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseTemperatureType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getBedTemperatureChannel(trayId), value));
findKey(map, "ctype")//
.map(Object::toString)//
.flatMap(StateParserHelper::parseDecimalType)//
.or(StateParserHelper::undef)//
.ifPresent(value -> updateState(AmsChannel.getCtypeChannel(trayId), value));
}
private static Optional<?> findKey(Map<?, ?> map, String key) {
return Optional.of(map).map(m -> m.get(key));
}
public int getAmsNumber() {
return requireNonNull(config, "At this point config should be initialized").number;
}
@Override
public void dispose() {
config = null;
logger = LoggerFactory.getLogger(AmsDeviceHandlerFactory.class);
}
}
@@ -59,7 +59,7 @@ public interface BambuHandler {
/**
* Returns current scheduler
*
*
* @return scheduler
*/
ScheduledExecutorService getScheduler();
@@ -12,9 +12,15 @@
*/
package org.openhab.binding.bambulab.internal;
import static java.util.Arrays.stream;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BambuLabBindingConstants} class defines common constants, which are
@@ -32,9 +38,11 @@ public class BambuLabBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID PRINTER_THING_TYPE = new ThingTypeUID(BINDING_ID, "printer");
public static final ThingTypeUID AMS_THING_TYPE = new ThingTypeUID(BINDING_ID, "ams-device");
@SuppressWarnings("StaticMethodOnlyUsedInOneClass")
public enum Channel {
CHANNEL_COMMAND("command", true),
CHANNEL_NOZZLE_TEMPERATURE("nozzle-temperature"),
CHANNEL_NOZZLE_TARGET_TEMPERATURE("nozzle-target-temperature"),
CHANNEL_BED_TEMPERATURE("bed-temperature"),
@@ -60,6 +68,28 @@ public class BambuLabBindingConstants {
CHANNEL_VIBRATION_CALIBRATION("vibration-calibration"),
CHANNEL_CAMERA_RECORD("camera-record", true),
CHANNEL_CAMERA_IMAGE("camera-image"),
// vtray
CHANNEL_VTRAY_TRAY_TYPE("vtray-tray-type"),
CHANNEL_VTRAY_TRAY_COLOR("vtray-tray-color"),
CHANNEL_VTRAY_NOZZLE_TEMPERATURE_MAX("vtray-nozzle-temperature-max"),
CHANNEL_VTRAY_NOZZLE_TEMPERATURE_MIN("vtray-nozzle-temperature-min"),
CHANNEL_VTRAY_REMAIN("vtray-remain"),
CHANNEL_VTRAY_K("vtray-k"),
CHANNEL_VTRAY_N("vtray-n"),
CHANNEL_VTRAY_TAG_UUID("vtray-tag-uuid"),
CHANNEL_VTRAY_TRAY_ID_NAME("vtray-tray-id-name"),
CHANNEL_VTRAY_TRAY_INFO_IDX("vtray-tray-info-idx"),
CHANNEL_VTRAY_TRAY_SUB_BRANDS("vtray-tray-sub-brands"),
CHANNEL_VTRAY_TRAY_WEIGHT("vtray-tray-weight"),
CHANNEL_VTRAY_TRAY_DIAMETER("vtray-tray-diameter"),
CHANNEL_VTRAY_TRAY_TEMPERATURE("vtray-tray-temperature"),
CHANNEL_VTRAY_TRAY_TIME("vtray-tray-time"),
CHANNEL_VTRAY_BED_TEMPERATURE_TYPE("vtray-bed-temp-type"),
CHANNEL_VTRAY_BED_TEMPERATURE("vtray-bed-temperature"),
// AMS generic
CHANNEL_AMS_TRAY_NOW("ams-tray-now"),
CHANNEL_AMS_TRAY_PREVIOUS("ams-tray-previous"),
// leds
CHANNEL_LED_CHAMBER_LIGHT("led-chamber", true),
CHANNEL_LED_WORK_LIGHT("led-work", true);
@@ -92,4 +122,122 @@ public class BambuLabBindingConstants {
return name.equals(channelUID.getId());
}
}
public static class AmsChannel {
public static final int MIN_AMS = 1;
/**
* According to Bambu Lab documentation, you can attach up to 4 AMS
*/
public static final int MAX_AMS = 4;
/**
* Each AMS device has 4 trays
*/
public static final int MAX_AMS_TRAYS = 4;
public static String getTrayTypeChannel(int trayId) {
return prefix(trayId) + "ams-tray-type";
}
public static String getTrayColorChannel(int trayId) {
return prefix(trayId) + "ams-tray-color";
}
public static String getNozzleTemperatureMaxChannel(int trayId) {
return prefix(trayId) + "ams-nozzle-temperature-max";
}
public static String getNozzleTemperatureMinChannel(int trayId) {
return prefix(trayId) + "ams-nozzle-temperature-min";
}
public static String getRemainChannel(int trayId) {
return prefix(trayId) + "ams-remain";
}
public static String getKChannel(int trayId) {
return prefix(trayId) + "ams-k";
}
public static String getNChannel(int trayId) {
return prefix(trayId) + "ams-n";
}
public static String getTagUuidChannel(int trayId) {
return prefix(trayId) + "ams-tag-uuid";
}
public static String getTrayIdNameChannel(int trayId) {
return prefix(trayId) + "ams-tray-id-name";
}
public static String getTrayInfoIdxChannel(int trayId) {
return prefix(trayId) + "ams-tray-info-idx";
}
public static String getTraySubBrandsChannel(int trayId) {
return prefix(trayId) + "ams-tray-sub-brands";
}
public static String getTrayWeightChannel(int trayId) {
return prefix(trayId) + "ams-tray-weight";
}
public static String getTrayDiameterChannel(int trayId) {
return prefix(trayId) + "ams-tray-diameter";
}
public static String getTrayTemperatureChannel(int trayId) {
return prefix(trayId) + "ams-tray-temperature";
}
public static String getTrayTimeChannel(int trayId) {
return prefix(trayId) + "ams-tray-time";
}
public static String getBedTemperatureTypeChannel(int trayId) {
return prefix(trayId) + "ams-bed-temp-type";
}
public static String getBedTemperatureChannel(int trayId) {
return prefix(trayId) + "ams-bed-temperature";
}
public static String getCtypeChannel(int trayId) {
return prefix(trayId) + "ams-ctype";
}
private static String prefix(int trayId) {
checkTrayId(trayId);
return "ams-tray-%s#".formatted(trayId + 1);
}
private static void checkTrayId(int trayId) {
if (trayId <= 0 || trayId > MAX_AMS_TRAYS) {
throw new IllegalArgumentException(
"Invalid tray ID: %d. Allowed range: 1 to %d.".formatted(trayId, MAX_AMS_TRAYS));
}
}
public static enum TrayType {
PLA,
PETG,
ABS,
TPU,
ASA,
PA,
PC,
PVA,
HIPS;
private static final Logger log = LoggerFactory.getLogger(TrayType.class);
public static Optional<TrayType> findTrayType(String name) {
var any = stream(values()).filter(t -> t.name().equalsIgnoreCase(name)).findAny();
if (any.isEmpty()) {
log.warn("Cannot parse TrayType from {}!", name);
}
return any;
}
}
}
}
@@ -12,12 +12,13 @@
*/
package org.openhab.binding.bambulab.internal;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.PRINTER_THING_TYPE;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -35,7 +36,7 @@ import org.osgi.service.component.annotations.Component;
@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);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PRINTER_THING_TYPE, AMS_THING_TYPE);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -46,8 +47,12 @@ public class BambuLabHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PRINTER_THING_TYPE.equals(thingTypeUID)) {
return new PrinterHandler(thing);
if (PRINTER_THING_TYPE.equals(thingTypeUID) && thing instanceof Bridge bridge) {
return new PrinterHandler(bridge);
}
if (AMS_THING_TYPE.equals(thingTypeUID)) {
return new AmsDeviceHandlerFactory(thing);
}
return null;
@@ -0,0 +1,204 @@
/*
* 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 pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand.LedMode.FLASHING;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class CommandParser {
public static PrinterClient.Channel.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(PrinterClient.Channel.InfoCommand.class.getSimpleName())) {
return parseInfoCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.PushingCommand.class.getSimpleName())) {
return parsePushingCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.PrintCommand.class.getSimpleName())) {
return parsePrintCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.ChangeFilamentCommand.class.getSimpleName())) {
return parseChangeFilamentCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.AmsUserSettingCommand.class.getSimpleName())) {
return parseAmsUserSettingCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.AmsFilamentSettingCommand.class.getSimpleName())) {
return parseAmsFilamentSettingCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.AmsControlCommand.class.getSimpleName())) {
return parseAmsControlCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.PrintSpeedCommand.class.getSimpleName())) {
return parsePrintSpeedCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.GCodeFileCommand.class.getSimpleName())) {
return parseGCodeFileCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.GCodeLineCommand.class.getSimpleName())) {
var gcodeLineSplit = stringCommand.split(":", 2);
requireLength(gcodeLineSplit, 2);
return parseGCodeLineCommand(gcodeLineSplit[1]);
}
if (commandName.equals(PrinterClient.Channel.LedControlCommand.class.getSimpleName())) {
return parseLedControlCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.SystemCommand.class.getSimpleName())) {
return parseSystemCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.IpCamRecordCommand.class.getSimpleName())) {
return parseIpCamRecordCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.IpCamTimelapsCommand.class.getSimpleName())) {
return parseIpCamTimelapsCommand(tail);
}
if (commandName.equals(PrinterClient.Channel.XCamControlCommand.class.getSimpleName())) {
return parseXCamControlCommand(tail);
}
throw new IllegalArgumentException("Unknown command name: " + commandName);
}
private static String[] tail(String[] command) {
return copyOfRange(command, 1, command.length);
}
private static 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 static PrinterClient.Channel.InfoCommand parseInfoCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrinterClient.Channel.InfoCommand.valueOf(commandLine[0]);
}
private static PrinterClient.Channel.PushingCommand parsePushingCommand(String[] commandLine) {
if (commandLine.length == 0) {
return PrinterClient.Channel.PushingCommand.defaultPushingCommand();
}
requireLength(commandLine, 2);
return new PrinterClient.Channel.PushingCommand(parseInt(commandLine[0]), parseInt(commandLine[1]));
}
private static PrinterClient.Channel.PrintCommand parsePrintCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrinterClient.Channel.PrintCommand.valueOf(commandLine[0]);
}
private static PrinterClient.Channel.ChangeFilamentCommand parseChangeFilamentCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new PrinterClient.Channel.ChangeFilamentCommand(parseInt(commandLine[0]), parseInt(commandLine[1]),
parseInt(commandLine[2]));
}
private static PrinterClient.Channel.AmsUserSettingCommand parseAmsUserSettingCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new PrinterClient.Channel.AmsUserSettingCommand(parseInt(commandLine[0]), parseBoolean(commandLine[1]),
parseBoolean(commandLine[2]));
}
private static PrinterClient.Channel.AmsFilamentSettingCommand parseAmsFilamentSettingCommand(
String[] commandLine) {
requireLength(commandLine, 7);
return new PrinterClient.Channel.AmsFilamentSettingCommand(parseInt(commandLine[0]), parseInt(commandLine[1]),
commandLine[2], commandLine[3], parseInt(commandLine[4]), parseInt(commandLine[5]), commandLine[6]);
}
private static PrinterClient.Channel.AmsControlCommand parseAmsControlCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrinterClient.Channel.AmsControlCommand.valueOf(commandLine[0]);
}
private static PrinterClient.Channel.PrintSpeedCommand parsePrintSpeedCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrinterClient.Channel.PrintSpeedCommand.valueOf(commandLine[0]);
}
private static PrinterClient.Channel.GCodeFileCommand parseGCodeFileCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new PrinterClient.Channel.GCodeFileCommand(commandLine[0]);
}
private static PrinterClient.Channel.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 PrinterClient.Channel.GCodeLineCommand(lines, split[0]);
}
private static PrinterClient.Channel.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 = PrinterClient.Channel.LedControlCommand.LedNode.valueOf(commandLine[0]);
var ledMode = PrinterClient.Channel.LedControlCommand.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 PrinterClient.Channel.LedControlCommand(ledNode, ledMode, ledOnTime, ledOffTime, loopTimes,
intervalTime);
}
private static PrinterClient.Channel.SystemCommand parseSystemCommand(String[] commandLine) {
requireLength(commandLine, 1);
return PrinterClient.Channel.SystemCommand.valueOf(commandLine[0]);
}
private static PrinterClient.Channel.IpCamRecordCommand parseIpCamRecordCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new PrinterClient.Channel.IpCamRecordCommand(parseBoolean(commandLine[0]));
}
private static PrinterClient.Channel.IpCamTimelapsCommand parseIpCamTimelapsCommand(String[] commandLine) {
requireLength(commandLine, 1);
return new PrinterClient.Channel.IpCamTimelapsCommand(parseBoolean(commandLine[0]));
}
private static PrinterClient.Channel.XCamControlCommand parseXCamControlCommand(String[] commandLine) {
requireLength(commandLine, 3);
return new PrinterClient.Channel.XCamControlCommand(
PrinterClient.Channel.XCamControlCommand.Module.valueOf(commandLine[0]), parseBoolean(commandLine[1]),
parseBoolean(commandLine[2]));
}
}
@@ -12,14 +12,8 @@
*/
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.mqtt.PrinterClient.Channel.LedControlCommand.LedMode.FLASHING;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -29,25 +23,6 @@ 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.mqtt.PrinterClient.Channel.AmsControlCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.AmsFilamentSettingCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.AmsUserSettingCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.ChangeFilamentCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.Command;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.GCodeFileCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.GCodeLineCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.InfoCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.IpCamRecordCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.IpCamTimelapsCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand.LedMode;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand.LedNode;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintSpeedCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PushingCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.SystemCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.XCamControlCommand;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@@ -74,176 +49,7 @@ public class PrinterActions implements ThingActions {
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]));
localHandler.sendCommand(stringCommand);
}
public static void sendCommand(@Nullable ThingActions actions, String stringCommand) {
@@ -36,6 +36,8 @@ public class PrinterConfiguration {
public String hostname = "";
public String accessCode = "";
public String username = LOCAL_USERNAME;
public int reconnectTime = 300;
public int reconnectMax = 5;
// MQTT
public String scheme = SCHEME;
public int port = DEFAULT_PORT;
@@ -73,7 +75,7 @@ public class PrinterConfiguration {
return new URI(rawUri);
} catch (URISyntaxException e) {
throw new InitializationException(CONFIGURATION_ERROR,
"@text/printer.handler.init.invalidHostname[\"%s\"]".formatted(rawUri), e);
"@text/printer.handler.init.invalidHostname [\"%s\"]".formatted(rawUri), e);
}
}
@@ -12,10 +12,17 @@
*/
package org.openhab.binding.bambulab.internal;
import static java.lang.Integer.parseInt;
import static java.util.Arrays.stream;
import static java.util.Collections.synchronizedList;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.function.Function.identity;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.AmsChannel.*;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.Channel.*;
import static org.openhab.binding.bambulab.internal.StateParserHelper.*;
import static org.openhab.binding.bambulab.internal.TrayHelper.updateTrayLoaded;
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.OFFLINE;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.thing.ThingStatus.UNKNOWN;
@@ -27,6 +34,7 @@ import static pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PushingCommand
import static pl.grzeslowski.jbambuapi.mqtt.PrinterClientConfig.requiredFields;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -34,28 +42,33 @@ import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Pattern;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
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.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzeslowski.jbambuapi.mqtt.ConnectionCallback;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.GCodeFileCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintSpeedCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClientConfig;
import pl.grzeslowski.jbambuapi.mqtt.PrinterWatcher;
import pl.grzeslowski.jbambuapi.mqtt.Report;
@@ -66,31 +79,54 @@ import pl.grzeslowski.jbambuapi.mqtt.Report;
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.StateSubscriber, BambuHandler {
private static final Pattern DBM_PATTERN = Pattern.compile("^(-?\\d+)dBm$");
public class PrinterHandler extends BaseBridgeHandler
implements PrinterWatcher.StateSubscriber, BambuHandler, ConnectionCallback {
private static final String INTERNAL_COMMAND_PREFIX = ">";
private Logger logger = LoggerFactory.getLogger(PrinterHandler.class);
private @Nullable PrinterClient client;
private @Nullable Camera camera;
private final AtomicInteger reconnectTimes = new AtomicInteger();
private final AtomicReference<@Nullable ScheduledFuture<?>> reconnectSchedule = new AtomicReference<>();
private final PrinterWatcher printerWatcher = new PrinterWatcher();
private @Nullable PrinterClientConfig config;
private final Collection<AmsDeviceHandlerFactory> amses = synchronizedList(new ArrayList<>());
private final AtomicReference<@Nullable Report> latestPrinterState = new AtomicReference<>();
public PrinterHandler(Thing thing) {
super(thing);
public PrinterHandler(Bridge bridge) {
super(bridge);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (CHANNEL_LED_CHAMBER_LIGHT.is(channelUID) || CHANNEL_LED_WORK_LIGHT.is(channelUID)) {
var ledNode = CHANNEL_LED_CHAMBER_LIGHT.getName().equals(channelUID.getId()) ? CHAMBER_LIGHT : WORK_LIGHT;
var ledNode = CHANNEL_LED_CHAMBER_LIGHT.is(channelUID) ? CHAMBER_LIGHT : WORK_LIGHT;
var bambuCommand = "ON".equals(command.toFullString()) ? on(ledNode) : off(ledNode);
sendCommand(bambuCommand);
} else if (CHANNEL_GCODE_FILE.is(channelUID)) {
var bambuCommand = new GCodeFileCommand(command.toString());
sendCommand(bambuCommand);
} else if (CHANNEL_SPEED_LEVEL.is(channelUID)) {
var bambuCommand = PrintSpeedCommand.valueOf(command.toString());
sendCommand(bambuCommand);
} else if (CHANNEL_SPEED_LEVEL.is(channelUID) && command instanceof StringType) {
stream(PrintSpeedCommand.values())//
.filter(type -> type.name().equalsIgnoreCase(command.toString()))//
.findAny()//
.ifPresent(this::sendCommand);
} else if (CHANNEL_CAMERA_RECORD.is(channelUID) && command instanceof OnOffType onOffCommand) {
requireNonNull(camera).handleCommand(onOffCommand);
} else if (CHANNEL_COMMAND.is(channelUID) && command instanceof StringType) {
var commandString = command.toString();
if (commandString.startsWith(INTERNAL_COMMAND_PREFIX)) {
logger.debug("Command {} updates just the state, ignoring...", commandString);
return;
}
String result;
try {
sendCommand(commandString);
result = INTERNAL_COMMAND_PREFIX + " SUCCESS: " + commandString;
} catch (Exception ex) {
result = INTERNAL_COMMAND_PREFIX + " ERROR: " + ex.getLocalizedMessage();
}
updateState(channelUID, StringType.valueOf(result));
}
}
@@ -104,7 +140,7 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
}
private void internalInitialize() throws InitializationException {
PrinterConfiguration config = getConfigAs(PrinterConfiguration.class);
var config = getConfigAs(PrinterConfiguration.class);
config.validateSerial();
logger = LoggerFactory.getLogger(PrinterHandler.class.getName() + "." + config.serial);
@@ -115,41 +151,88 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
// always turn off camera recording when starting thing
camera = new Camera(config, this);
updateState(CHANNEL_CAMERA_RECORD.getName(), OnOffType.OFF);
updateState(CHANNEL_CAMERA_IMAGE.getName(), UNDEF);
updateState(CHANNEL_CAMERA_RECORD, OnOffType.OFF);
updateState(CHANNEL_CAMERA_IMAGE, UNDEF);
// turn off the command channel
updateState(CHANNEL_COMMAND, StringType.valueOf(INTERNAL_COMMAND_PREFIX + " Ready to accept commands"));
var localClient = client = buildLocalClient(uri, config);
// the status will be unknown until the first message form MQTT arrives
updateStatus(UNKNOWN);
try {
scheduler.execute(() -> initMqtt(localClient));
scheduler.execute(() -> initMqtt(localClient, config));
} catch (RejectedExecutionException ex) {
logger.debug("Task was rejected", ex);
throw new InitializationException(CONFIGURATION_ERROR, ex);
}
}
private void initMqtt(PrinterClient client) {
private void initMqtt(PrinterClient client, PrinterConfiguration config) {
try {
logger.debug("Trying to connect to the printer broker");
client.connect();
var printerWatcher = new PrinterWatcher();
client.connect(this);
client.subscribe(printerWatcher);
printerWatcher.subscribe(this);
// send request to update all channels
refreshChannels();
updateStatus(ONLINE);
// update to online done in `connectComplete`
} catch (Exception e) {
logger.debug("Cannot connect to MQTT client", e);
updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
reconnect(config);
}
}
@Override
public void connectComplete(boolean reconnect) {
logger.debug("connectComplete({})", reconnect);
// send request to update all channels
scheduler.schedule(this::refreshChannels, 1, SECONDS);
reconnectTimes.set(0);
updateStatus(ONLINE);
}
@Override
public void connectionLost(@Nullable Throwable throwable) {
logger.debug("Connection lost. Restarting thing", throwable);
var message = throwable != null ? throwable.getLocalizedMessage() : "<no message" + INTERNAL_COMMAND_PREFIX;
var description = "@text/printer.handler.init.connectionLost [\"%s\"]".formatted(message);
updateStatus(OFFLINE, COMMUNICATION_ERROR, description);
reconnect(null);
}
private void reconnect(@Nullable PrinterConfiguration config) {
if (reconnectSchedule.get() != null) {
logger.warn("Already reconnecting!");
return;
}
var configNotNull = Optional.ofNullable(config)//
.orElseGet(() -> getConfigAs(PrinterConfiguration.class));
var currentReconnections = reconnectTimes.getAndAdd(1);
if (currentReconnections >= configNotNull.reconnectMax) {
logger.warn("Do not reconnecting any more, because max reconnections was reach!");
return;
}
logger.debug("Will reconnect in {} seconds...", configNotNull.reconnectTime);
try {
reconnectSchedule.set(//
scheduler.schedule(() -> {
logger.debug("Reconnecting...");
reconnectSchedule.set(null);
dispose();
initialize();
}, configNotNull.reconnectTime, SECONDS));
} catch (RejectedExecutionException ex) {
logger.debug("Task was rejected", ex);
updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
}
}
private PrinterClient buildLocalClient(URI uri, PrinterConfiguration config) throws InitializationException {
try {
return new PrinterClient(
requiredFields(uri, config.username, config.serial, config.accessCode.toCharArray()));
var localConfig = this.config = requiredFields(uri, config.username, config.serial,
config.accessCode.toCharArray());
return new PrinterClient(localConfig);
} catch (Exception e) {
logger.debug("Cannot create MQTT client", e);
throw new InitializationException(COMMUNICATION_ERROR, e);
@@ -163,28 +246,60 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
@Override
public void dispose() {
try {
var localClient = client;
client = null;
if (localClient != null) {
try {
localClient.close();
} catch (Exception e) {
logger.warn("Could not correctly dispose PrinterClient", e);
}
}
var localCamera = camera;
camera = null;
if (localCamera != null) {
localCamera.close();
}
latestPrinterState.set(null);
printerWatcher.close();
amses.clear();
closeConfig();
closeReconnectSchedule();
closeCamera();
closeClient();
} finally {
logger = LoggerFactory.getLogger(PrinterHandler.class);
}
}
private void closeConfig() {
var localConfig = config;
config = null;
if (localConfig != null) {
localConfig.close();
}
}
private void closeClient() {
var localClient = client;
client = null;
if (localClient != null) {
try {
localClient.close();
} catch (Exception e) {
logger.warn("Could not correctly dispose PrinterClient", e);
}
}
}
private void closeCamera() {
var localCamera = camera;
camera = null;
if (localCamera != null) {
localCamera.close();
}
}
private void closeReconnectSchedule() {
var localReconnectSchedule = reconnectSchedule.get();
reconnectSchedule.set(null);
if (localReconnectSchedule != null) {
localReconnectSchedule.cancel(true);
}
}
@Override
public void newState(@Nullable Report delta, @Nullable Report fullState) {
logger.trace("New Printer state from delta {}", delta);
if (fullState != null) {
latestPrinterState.set(fullState);
}
// 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) {
@@ -202,66 +317,159 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
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());
updateCelsiusState(CHANNEL_NOZZLE_TEMPERATURE, print.nozzleTemper());
updateCelsiusState(CHANNEL_NOZZLE_TARGET_TEMPERATURE, print.nozzleTargetTemper());
updateCelsiusState(CHANNEL_BED_TEMPERATURE, print.bedTemper());
updateCelsiusState(CHANNEL_BED_TARGET_TEMPERATURE, print.bedTargetTemper());
updateCelsiusState(CHANNEL_CHAMBER_TEMPERATURE, 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());
updateStringState(CHANNEL_MC_PRINT_STAGE, print.mcPrintStage());
updateStringState(CHANNEL_BED_TYPE, print.bedType());
updateStringState(CHANNEL_GCODE_FILE, print.gcodeFile());
updateStringState(CHANNEL_GCODE_STATE, print.gcodeState());
updateStringState(CHANNEL_REASON, print.reason());
updateStringState(CHANNEL_RESULT, print.result());
// percent
updatePercentState(CHANNEL_MC_PERCENT.getName(), print.mcPercent());
updatePercentState(CHANNEL_GCODE_FILE_PREPARE_PERCENT.getName(), print.gcodeFilePreparePercent());
updatePercentState(CHANNEL_MC_PERCENT, print.mcPercent());
updatePercentState(CHANNEL_GCODE_FILE_PREPARE_PERCENT, 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());
parseTimeMinutes(print.mcRemainingTime())//
.ifPresent(time -> updateState(CHANNEL_MC_REMAINING_TIME, time));
updateDecimalState(CHANNEL_BIG_FAN_1_SPEED, print.bigFan1Speed());
updateDecimalState(CHANNEL_BIG_FAN_2_SPEED, print.bigFan2Speed());
updateDecimalState(CHANNEL_HEAT_BREAK_FAN_SPEED, print.heatbreakFanSpeed());
updateDecimalState(CHANNEL_LAYER_NUM, print.layerNum());
if (print.spdLvl() != null) {
var speedLevel = PrintSpeedCommand.findByLevel(print.spdLvl());
updateState(CHANNEL_SPEED_LEVEL.getName(), new StringType(speedLevel.toString()));
updateState(CHANNEL_SPEED_LEVEL, new StringType(speedLevel.toString()));
}
// boolean
updateBooleanState(CHANNEL_TIME_LAPS.getName(), print.timelapse());
updateBooleanState(CHANNEL_USE_AMS.getName(), print.useAms());
updateBooleanState(CHANNEL_VIBRATION_CALIBRATION.getName(), print.vibrationCali());
updateBooleanState(CHANNEL_TIME_LAPS, print.timelapse());
updateBooleanState(CHANNEL_USE_AMS, print.useAms());
updateBooleanState(CHANNEL_VIBRATION_CALIBRATION, print.vibrationCali());
// lights
updateLightState("chamber_light", CHANNEL_LED_CHAMBER_LIGHT, print.lightsReport());
updateLightState("work_light", CHANNEL_LED_WORK_LIGHT, print.lightsReport());
// other
if (print.wifiSignal() != null) {
updateState(CHANNEL_WIFI_SIGNAL.getName(), parseWifiChannel(print.wifiSignal()));
updateState(CHANNEL_WIFI_SIGNAL, parseWifiChannel(print.wifiSignal()));
}
// ams
Optional.of(print)//
.map(Report.Print::ams)//
.ifPresent(ams -> {
updateState(CHANNEL_AMS_TRAY_NOW, updateTrayLoaded(ams.trayNow()));
updateState(CHANNEL_AMS_TRAY_PREVIOUS, updateTrayLoaded(ams.trayPre()));
});
Optional.of(print)//
.map(Report.Print::ams)//
.map(Report.Print.Ams::ams)//
.stream()//
.flatMap(Collection::stream)//
.forEach(this::updateAms);
// vtray
Optional.of(print)//
.map(Report.Print::vtTray)//
.ifPresent(this::updateVtray);
}
private void updateCelsiusState(String channelId, @Nullable Double temperature) {
private void updateVtray(Report.Print.VtTray vtTray) {
Optional.ofNullable(vtTray.trayType())//
.map(Object::toString)//
.flatMap(TrayType::findTrayType)//
.map(Enum::name)//
.flatMap(StateParserHelper::parseStringType)//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_TYPE, trayType));
Optional.ofNullable(vtTray.trayColor())//
.map(Object::toString)//
.map(StateParserHelper::parseColor)//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_COLOR, trayType));
parseTemperatureType(vtTray.nozzleTempMax())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_NOZZLE_TEMPERATURE_MAX, trayType));
parseTemperatureType(vtTray.nozzleTempMin())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_NOZZLE_TEMPERATURE_MIN, trayType));
Optional.ofNullable(vtTray.remain())//
.flatMap(StateParserHelper::parsePercentType)//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_REMAIN, trayType));
parseDecimalType(vtTray.k())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_K, trayType));
parseDecimalType(vtTray.n())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_N, trayType));
parseStringType(vtTray.tagUid())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TAG_UUID, trayType));
parseStringType(vtTray.trayIdName())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_ID_NAME, trayType));
parseStringType(vtTray.trayInfoIdx())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_INFO_IDX, trayType));
parseStringType(vtTray.traySubBrands())
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_SUB_BRANDS, trayType));
parseDecimalType(vtTray.trayWeight())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_WEIGHT, trayType));
parseDecimalType(vtTray.trayDiameter())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_DIAMETER, trayType));
parseTemperatureType(vtTray.trayTemp())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_TEMPERATURE, trayType));
parseDecimalType(vtTray.trayTime())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_TRAY_TIME, trayType));
parseStringType(vtTray.bedTempType())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_BED_TEMPERATURE_TYPE, trayType));
parseTemperatureType(vtTray.bedTemp())//
.or(StateParserHelper::undef)//
.ifPresent(trayType -> updateState(CHANNEL_VTRAY_BED_TEMPERATURE, trayType));
}
private void updateAms(Map<String, Object> amsMap) {
Optional.of(amsMap)//
.map(map -> map.get("id"))//
.map(Object::toString)//
.map(Integer::parseInt)//
// in code, we are using 1-4 ordering; in API 0-3 ordering is used
.map(id -> id + 1) //
.filter(id -> id >= MIN_AMS)//
.filter(id -> id <= MAX_AMS)//
.map(id -> amses.stream().filter(ams -> ams.getAmsNumber() == id))//
.stream()//
.flatMap(identity())//
.forEach(ams -> ams.updateAms(amsMap));
}
private void updateCelsiusState(BambuLabBindingConstants.Channel channelId, @Nullable Double temperature) {
if (temperature == null) {
return;
}
updateState(channelId, new QuantityType<>(temperature, CELSIUS));
}
private void updateStringState(String channelId, @Nullable String string) {
private void updateStringState(BambuLabBindingConstants.Channel channelId, @Nullable String string) {
if (string == null) {
return;
}
updateState(channelId, new StringType(string));
}
private void updateDecimalState(String channelId, @Nullable Number number) {
private void updateDecimalState(BambuLabBindingConstants.Channel channelId, @Nullable Number number) {
if (number == null) {
return;
}
updateState(channelId, new DecimalType(number));
}
private void updateDecimalState(String channelId, @Nullable String number) {
private void updateDecimalState(BambuLabBindingConstants.Channel channelId, @Nullable String number) {
if (number == null) {
return;
}
@@ -274,31 +482,19 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
}
}
private void updateBooleanState(String channelId, @Nullable Boolean bool) {
private void updateBooleanState(BambuLabBindingConstants.Channel 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(BambuLabBindingConstants.Channel channelId, @Nullable Integer integer) {
parsePercentType(integer).ifPresent(state -> updateState(channelId, state));
}
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 void updatePercentState(BambuLabBindingConstants.Channel channelId, @Nullable String integer) {
parsePercentType(integer).ifPresent(state -> updateState(channelId, state));
}
private void updateLightState(String lightName, BambuLabBindingConstants.Channel channel,
@@ -311,23 +507,11 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
.filter(Objects::nonNull)//
.map(OnOffType::from)//
.findAny()//
.ifPresent(command -> updateState(channel.getName(), command));
.ifPresent(command -> updateState(channel, command));
}
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(String command) {
sendCommand(CommandParser.parseCommand(command));
}
public void sendCommand(PrinterClient.Channel.Command command) {
@@ -344,6 +528,10 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
}
}
private void updateState(BambuLabBindingConstants.Channel channelID, State state) {
updateState(channelID.getName(), state);
}
@Override
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
super.updateStatus(status, statusDetail, description);
@@ -368,4 +556,38 @@ public class PrinterHandler extends BaseThingHandler implements PrinterWatcher.S
public Logger getLogger() {
return logger;
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
if (!(childHandler instanceof AmsDeviceHandlerFactory ams)) {
return;
}
amses.add(ams);
Optional.of(latestPrinterState)//
.map(AtomicReference::get)//
.map(Report::print).map(Report.Print::ams)//
.map(Report.Print.Ams::ams)//
.stream()//
.flatMap(Collection::stream)//
.filter(map -> map.containsKey("id"))//
.filter(map -> map.get("id") != null)//
// in code, we are using 1-4 ordering; in API 0-3 ordering is used
.filter(map -> parseInt(map.get("id").toString()) + 1 == ams.getAmsNumber())//
.forEach(ams::updateAms);
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
if (!(childHandler instanceof AmsDeviceHandlerFactory ams)) {
return;
}
var removed = amses.remove(ams);
if (!removed) {
logger.warn("Did not remove ams handler {}", ams);
}
}
public String getSerialNumber() {
return requireNonNull(config).serial();
}
}
@@ -0,0 +1,118 @@
/*
* 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.Integer.parseInt;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.Units.*;
import static org.openhab.core.types.UnDefType.UNDEF;
import static tech.units.indriya.unit.Units.PERCENT;
import java.util.Optional;
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.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
public class StateParserHelper {
private static final Pattern DBM_PATTERN = Pattern.compile("^(-?\\d+)dBm$");
private static final Logger logger = LoggerFactory.getLogger(StateParserHelper.class);
public static Optional<State> parseTimeMinutes(@Nullable Integer integer) {
return Optional.ofNullable(integer)//
.map(time -> new QuantityType<>(time, MINUTE));
}
public static Optional<State> parseTemperatureType(@Nullable String value) {
try {
return Optional.ofNullable(value).map(Double::parseDouble).map(d -> new QuantityType<>(d, CELSIUS));
} catch (NumberFormatException ex) {
logger.debug("Cannot parse: {}", value, ex);
return Optional.of(UNDEF);
}
}
public static Optional<State> parseDecimalType(@Nullable Number number) {
return Optional.ofNullable(number).map(DecimalType::new);
}
public static Optional<State> parseDecimalType(@Nullable String value) {
try {
return Optional.ofNullable(value).map(DecimalType::new);
} catch (NumberFormatException ex) {
logger.debug("Cannot parse {}", value, ex);
return Optional.of(UNDEF);
}
}
public static Optional<State> parsePercentType(@Nullable Number percent) {
return Optional.ofNullable(percent).map(d -> new QuantityType<>(d, PERCENT));
}
public static Optional<State> parsePercentType(@Nullable String percent) {
try {
return Optional.ofNullable(percent)//
.map(Double::parseDouble)//
.map(d -> new QuantityType<>(d, PERCENT));
} catch (NumberFormatException ex) {
logger.debug("Cannot parse {}", percent, ex);
return Optional.of(UNDEF);
}
}
public static Optional<State> parseStringType(@Nullable String string) {
return Optional.ofNullable(string).map(StringType::new);
}
public static State parseWifiChannel(String wifi) {
var matcher = DBM_PATTERN.matcher(wifi);
if (!matcher.matches()) {
logger.debug("Cannot match {} to {}", wifi, DBM_PATTERN);
return UNDEF;
}
var integer = matcher.group(1);
try {
var value = parseInt(integer);
return new QuantityType<>(value, DECIBEL_MILLIWATTS);
} catch (NumberFormatException e) {
logger.debug("Cannot parse integer {} from wifi {}", integer, wifi, e);
return UNDEF;
}
}
public static State parseColor(String colorHex) {
if (colorHex.length() < 6) {
return UNDEF;
}
int r = parseInt(colorHex.substring(0, 2), 16);
int g = parseInt(colorHex.substring(2, 4), 16);
int b = parseInt(colorHex.substring(4, 6), 16);
return ColorUtil.rgbToHsb(new int[] { r, g, b });
}
public static Optional<State> undef() {
return Optional.of(UNDEF);
}
}
@@ -0,0 +1,65 @@
/*
* 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.Integer.parseInt;
import static org.openhab.binding.bambulab.internal.BambuLabBindingConstants.AmsChannel.*;
import static org.openhab.core.types.UnDefType.UNDEF;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
class TrayHelper {
static final int MAX_TRAY_VALUE = MAX_AMS * MAX_AMS_TRAYS - 1;
private static final Logger logger = LoggerFactory.getLogger(TrayHelper.class);
static State updateTrayLoaded(@Nullable String tray) {
if (tray == null) {
return UNDEF;
}
try {
var integer = parseInt(tray);
return findStateForTrayLoaded(integer);
} catch (NumberFormatException e) {
logger.debug("Cannot parse: {}", tray, e);
return UNDEF;
}
}
private static State findStateForTrayLoaded(int tray) {
if (tray < 0) {
return UNDEF;
}
if (tray == 255) {
return StringType.valueOf("EMPTY");
}
if (tray == 254) {
return StringType.valueOf("VTRAY");
}
if (tray > MAX_TRAY_VALUE) {
logger.warn("There should never be tray with value {}", tray);
return UNDEF;
}
var amsNr = (tray / MAX_AMS) + 1;
var trayNr = (tray % MAX_AMS_TRAYS) + 1;
return StringType.valueOf("AMS_%s_%s".formatted(amsNr, trayNr));
}
}
@@ -5,8 +5,15 @@ addon.bambulab.description = This is the binding for BambuLab.
# thing types
thing-type.bambulab.ams-device.label = Bambu Lab AMS Device
thing-type.bambulab.ams-device.description = Represents a Bambu Lab AMS connected to a printer.
thing-type.bambulab.ams-device.group.ams-tray-1.label = AMS Tray #1
thing-type.bambulab.ams-device.group.ams-tray-2.label = AMS Tray #2
thing-type.bambulab.ams-device.group.ams-tray-3.label = AMS Tray #3
thing-type.bambulab.ams-device.group.ams-tray-4.label = AMS Tray #4
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.ams-tray-previous.label = Previously Loaded Tray
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
@@ -21,6 +28,8 @@ thing-type.bambulab.printer.channel.camera-record.label = Camera Record
thing-type.bambulab.printer.channel.camera-record.description = Turns on/off openHAB's capability of recording.
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.command.label = Command
thing-type.bambulab.printer.channel.command.description = Send command to run. See <a href="https://github.com/openhab/openhab-addons/tree/main/bundles/org.openhab.binding.bambulab#sendcommand">Actions > sendCommand</a>
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
@@ -55,11 +64,31 @@ 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.vtray-bed-temp-type.label = Bed Temperature Type
thing-type.bambulab.printer.channel.vtray-bed-temperature.label = Bed Temperature
thing-type.bambulab.printer.channel.vtray-k.label = k
thing-type.bambulab.printer.channel.vtray-n.label = n
thing-type.bambulab.printer.channel.vtray-nozzle-temperature-max.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.vtray-nozzle-temperature-max.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.vtray-nozzle-temperature-min.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.vtray-nozzle-temperature-min.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.vtray-remain.label = Remain
thing-type.bambulab.printer.channel.vtray-remain.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.vtray-tag-uuid.label = Tag UUID
thing-type.bambulab.printer.channel.vtray-tray-diameter.label = Tray Diameter
thing-type.bambulab.printer.channel.vtray-tray-id-name.label = Tray ID Name
thing-type.bambulab.printer.channel.vtray-tray-info-idx.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.vtray-tray-sub-brands.label = Tray Sub Brands
thing-type.bambulab.printer.channel.vtray-tray-temperature.label = Tray Temperature
thing-type.bambulab.printer.channel.vtray-tray-time.label = Tray Time
thing-type.bambulab.printer.channel.vtray-tray-weight.label = Tray Weight
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.ams-device.number.label = AMS Number
thing-type.config.bambulab.ams-device.number.description = Order of the AMS
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.cameraCertificate.label = TLS Certificate
@@ -74,6 +103,10 @@ 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.reconnectMax.label = Max Reconnections
thing-type.config.bambulab.printer.reconnectMax.description = Max times to try reconnections.
thing-type.config.bambulab.printer.reconnectTime.label = Reconnect Time
thing-type.config.bambulab.printer.reconnectTime.description = Time in seconds to wait before reconnect.
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
@@ -86,18 +119,38 @@ thing-type.config.bambulab.printer.series.option.X = X Series
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 group types
channel-group-type.bambulab.ams-tray.label = AMS Tray
channel-group-type.bambulab.ams-tray.description = This channel group represents a single AMS tray.
channel-group-type.bambulab.ams-tray.channel.ams-bed-temp-type.label = Bed Temperature Type
channel-group-type.bambulab.ams-tray.channel.ams-bed-temperature.label = Bed Temperature
channel-group-type.bambulab.ams-tray.channel.ams-ctype.label = ctype
channel-group-type.bambulab.ams-tray.channel.ams-k.label = k
channel-group-type.bambulab.ams-tray.channel.ams-n.label = n
channel-group-type.bambulab.ams-tray.channel.ams-nozzle-temperature-max.label = Nozzle Temperature Max
channel-group-type.bambulab.ams-tray.channel.ams-nozzle-temperature-max.description = Maximum nozzle temperature for tray.
channel-group-type.bambulab.ams-tray.channel.ams-nozzle-temperature-min.label = Nozzle Temperature Min
channel-group-type.bambulab.ams-tray.channel.ams-nozzle-temperature-min.description = Minimum nozzle temperature for tray.
channel-group-type.bambulab.ams-tray.channel.ams-remain.label = Remain
channel-group-type.bambulab.ams-tray.channel.ams-remain.description = Amount of remaining filament.
channel-group-type.bambulab.ams-tray.channel.ams-tag-uuid.label = Tag UUID
channel-group-type.bambulab.ams-tray.channel.ams-tray-diameter.label = Tray Diameter
channel-group-type.bambulab.ams-tray.channel.ams-tray-id-name.label = Tray ID Name
channel-group-type.bambulab.ams-tray.channel.ams-tray-info-idx.label = Tray Info IDX Name
channel-group-type.bambulab.ams-tray.channel.ams-tray-sub-brands.label = Tray Sub Brands
channel-group-type.bambulab.ams-tray.channel.ams-tray-temperature.label = Tray Temperature
channel-group-type.bambulab.ams-tray.channel.ams-tray-time.label = Tray Time
channel-group-type.bambulab.ams-tray.channel.ams-tray-weight.label = Tray Weight
# channel types
channel-type.bambulab.boolean-rw.label = Boolean Channel
channel-type.bambulab.boolean.label = Boolean Channel
channel-type.bambulab.camera-image.label = Camera Image
channel-type.bambulab.camera-image.description = Current image from the printer's camera.
channel-type.bambulab.number-advanced.label = Advanced Number 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.speed-level.label = Print Speed Level
channel-type.bambulab.speed-level.description = Current speed setting of the print job.
@@ -109,18 +162,687 @@ channel-type.bambulab.speed-level.command.option.SILENT = Silent
channel-type.bambulab.speed-level.command.option.STANDARD = Standard
channel-type.bambulab.speed-level.command.option.SPORT = Sport
channel-type.bambulab.speed-level.command.option.LUDICROUS = Ludicrous
channel-type.bambulab.string-advanced.label = Advanced String Channel
channel-type.bambulab.string-rw.label = RW String Channel
channel-type.bambulab.string.label = String Channel
channel-type.bambulab.temperature-measurement-advanced.label = Current Temperature
channel-type.bambulab.temperature-measurement.label = Current Temperature
channel-type.bambulab.temperature-setpoint.label = Target Temperature
channel-type.bambulab.time-min.label = Time (min)
channel-type.bambulab.time-min.state.pattern = %1$tHh:%1$tMm
channel-type.bambulab.tray-color.label = Tray Color
channel-type.bambulab.tray-loaded.label = Currently Loaded Tray
channel-type.bambulab.tray-loaded.state.option.EMPTY = No tray loaded
channel-type.bambulab.tray-loaded.state.option.VTRAY = External Spool
channel-type.bambulab.tray-loaded.state.option.AMS_1_1 = AMS #1 / Tray 1
channel-type.bambulab.tray-loaded.state.option.AMS_1_2 = AMS #1 / Tray 2
channel-type.bambulab.tray-loaded.state.option.AMS_1_3 = AMS #1 / Tray 3
channel-type.bambulab.tray-loaded.state.option.AMS_1_4 = AMS #1 / Tray 4
channel-type.bambulab.tray-loaded.state.option.AMS_2_1 = AMS #2 / Tray 1
channel-type.bambulab.tray-loaded.state.option.AMS_2_2 = AMS #2 / Tray 2
channel-type.bambulab.tray-loaded.state.option.AMS_2_3 = AMS #2 / Tray 3
channel-type.bambulab.tray-loaded.state.option.AMS_2_4 = AMS #2 / Tray 4
channel-type.bambulab.tray-loaded.state.option.AMS_3_1 = AMS #3 / Tray 1
channel-type.bambulab.tray-loaded.state.option.AMS_3_2 = AMS #3 / Tray 2
channel-type.bambulab.tray-loaded.state.option.AMS_3_3 = AMS #3 / Tray 3
channel-type.bambulab.tray-loaded.state.option.AMS_3_4 = AMS #3 / Tray 4
channel-type.bambulab.tray-loaded.state.option.AMS_4_1 = AMS #4 / Tray 1
channel-type.bambulab.tray-loaded.state.option.AMS_4_2 = AMS #4 / Tray 2
channel-type.bambulab.tray-loaded.state.option.AMS_4_3 = AMS #4 / Tray 3
channel-type.bambulab.tray-loaded.state.option.AMS_4_4 = AMS #4 / Tray 4
channel-type.bambulab.tray-type.label = Tray Type
channel-type.bambulab.tray-type.state.option.PLA = PLA
channel-type.bambulab.tray-type.state.option.PETG = PETG
channel-type.bambulab.tray-type.state.option.ABS = ABS
channel-type.bambulab.tray-type.state.option.TPU = TPU
channel-type.bambulab.tray-type.state.option.ASA = ASA
channel-type.bambulab.tray-type.state.option.PA = PA
channel-type.bambulab.tray-type.state.option.PC = PC
channel-type.bambulab.tray-type.state.option.PVA = PVA
channel-type.bambulab.tray-type.state.option.HIPS = HIPS
channel-type.bambulab.wifi.label = Wi-Fi Signal Strength
channel-type.bambulab.wifi.description = Current Wi-Fi signal strength.
# thing types
thing-type.bambulab.ams-device.channel.ams-bed-temp-type-id-1.label = Bed Temperature Type
thing-type.bambulab.ams-device.channel.ams-bed-temp-type-id-2.label = Bed Temperature Type
thing-type.bambulab.ams-device.channel.ams-bed-temp-type-id-3.label = Bed Temperature Type
thing-type.bambulab.ams-device.channel.ams-bed-temp-type-id-4.label = Bed Temperature Type
thing-type.bambulab.ams-device.channel.ams-bed-temperature-id-1.label = Bed Temperature
thing-type.bambulab.ams-device.channel.ams-bed-temperature-id-2.label = Bed Temperature
thing-type.bambulab.ams-device.channel.ams-bed-temperature-id-3.label = Bed Temperature
thing-type.bambulab.ams-device.channel.ams-bed-temperature-id-4.label = Bed Temperature
thing-type.bambulab.ams-device.channel.ams-ctype-id-1.label = ctype
thing-type.bambulab.ams-device.channel.ams-ctype-id-2.label = ctype
thing-type.bambulab.ams-device.channel.ams-ctype-id-3.label = ctype
thing-type.bambulab.ams-device.channel.ams-ctype-id-4.label = ctype
thing-type.bambulab.ams-device.channel.ams-k-id-1.label = k
thing-type.bambulab.ams-device.channel.ams-k-id-2.label = k
thing-type.bambulab.ams-device.channel.ams-k-id-3.label = k
thing-type.bambulab.ams-device.channel.ams-k-id-4.label = k
thing-type.bambulab.ams-device.channel.ams-n-id-1.label = n
thing-type.bambulab.ams-device.channel.ams-n-id-2.label = n
thing-type.bambulab.ams-device.channel.ams-n-id-3.label = n
thing-type.bambulab.ams-device.channel.ams-n-id-4.label = n
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-1.label = Nozzle Temperature Max
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-2.label = Nozzle Temperature Max
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-3.label = Nozzle Temperature Max
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-4.label = Nozzle Temperature Max
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-max-id-4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-1.label = Nozzle Temperature Min
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-2.label = Nozzle Temperature Min
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-3.label = Nozzle Temperature Min
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-4.label = Nozzle Temperature Min
thing-type.bambulab.ams-device.channel.ams-nozzle-temperature-min-id-4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams-device.channel.ams-remain-id-1.label = Remain
thing-type.bambulab.ams-device.channel.ams-remain-id-1.description = Amount of remaining filament.
thing-type.bambulab.ams-device.channel.ams-remain-id-2.label = Remain
thing-type.bambulab.ams-device.channel.ams-remain-id-2.description = Amount of remaining filament.
thing-type.bambulab.ams-device.channel.ams-remain-id-3.label = Remain
thing-type.bambulab.ams-device.channel.ams-remain-id-3.description = Amount of remaining filament.
thing-type.bambulab.ams-device.channel.ams-remain-id-4.label = Remain
thing-type.bambulab.ams-device.channel.ams-remain-id-4.description = Amount of remaining filament.
thing-type.bambulab.ams-device.channel.ams-tag-uuid-id-1.label = Tag UUID
thing-type.bambulab.ams-device.channel.ams-tag-uuid-id-2.label = Tag UUID
thing-type.bambulab.ams-device.channel.ams-tag-uuid-id-3.label = Tag UUID
thing-type.bambulab.ams-device.channel.ams-tag-uuid-id-4.label = Tag UUID
thing-type.bambulab.ams-device.channel.ams-tray-diameter-id-1.label = Tray Diameter
thing-type.bambulab.ams-device.channel.ams-tray-diameter-id-2.label = Tray Diameter
thing-type.bambulab.ams-device.channel.ams-tray-diameter-id-3.label = Tray Diameter
thing-type.bambulab.ams-device.channel.ams-tray-diameter-id-4.label = Tray Diameter
thing-type.bambulab.ams-device.channel.ams-tray-id-name-id-1.label = Tray ID Name
thing-type.bambulab.ams-device.channel.ams-tray-id-name-id-2.label = Tray ID Name
thing-type.bambulab.ams-device.channel.ams-tray-id-name-id-3.label = Tray ID Name
thing-type.bambulab.ams-device.channel.ams-tray-id-name-id-4.label = Tray ID Name
thing-type.bambulab.ams-device.channel.ams-tray-info-idx-id-1.label = Tray Info IDX Name
thing-type.bambulab.ams-device.channel.ams-tray-info-idx-id-2.label = Tray Info IDX Name
thing-type.bambulab.ams-device.channel.ams-tray-info-idx-id-3.label = Tray Info IDX Name
thing-type.bambulab.ams-device.channel.ams-tray-info-idx-id-4.label = Tray Info IDX Name
thing-type.bambulab.ams-device.channel.ams-tray-sub-brands-id-1.label = Tray Sub Brands
thing-type.bambulab.ams-device.channel.ams-tray-sub-brands-id-2.label = Tray Sub Brands
thing-type.bambulab.ams-device.channel.ams-tray-sub-brands-id-3.label = Tray Sub Brands
thing-type.bambulab.ams-device.channel.ams-tray-sub-brands-id-4.label = Tray Sub Brands
thing-type.bambulab.ams-device.channel.ams-tray-temperature-id-1.label = Tray Temperature
thing-type.bambulab.ams-device.channel.ams-tray-temperature-id-2.label = Tray Temperature
thing-type.bambulab.ams-device.channel.ams-tray-temperature-id-3.label = Tray Temperature
thing-type.bambulab.ams-device.channel.ams-tray-temperature-id-4.label = Tray Temperature
thing-type.bambulab.ams-device.channel.ams-tray-time-id-1.label = Tray Time
thing-type.bambulab.ams-device.channel.ams-tray-time-id-2.label = Tray Time
thing-type.bambulab.ams-device.channel.ams-tray-time-id-3.label = Tray Time
thing-type.bambulab.ams-device.channel.ams-tray-time-id-4.label = Tray Time
thing-type.bambulab.ams-device.channel.ams-tray-weight-id-1.label = Tray Weight
thing-type.bambulab.ams-device.channel.ams-tray-weight-id-2.label = Tray Weight
thing-type.bambulab.ams-device.channel.ams-tray-weight-id-3.label = Tray Weight
thing-type.bambulab.ams-device.channel.ams-tray-weight-id-4.label = Tray Weight
# thing types
thing-type.bambulab.ams-device.channel.ams-tray-previous.label = Previously Loaded Tray
# channel types
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.tray-type.command.option.PLA = PLA
channel-type.bambulab.tray-type.command.option.PETG = PETG
channel-type.bambulab.tray-type.command.option.ABS = ABS
channel-type.bambulab.tray-type.command.option.TPU = TPU
channel-type.bambulab.tray-type.command.option.ASA = ASA
channel-type.bambulab.tray-type.command.option.PA = PA
channel-type.bambulab.tray-type.command.option.PC = PC
channel-type.bambulab.tray-type.command.option.PVA = PVA
channel-type.bambulab.tray-type.command.option.HIPS = HIPS
# thing types
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-1x1.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-1x2.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-1x3.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-1x4.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-2x1.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-2x2.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-2x3.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-2x4.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-3x1.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-3x2.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-3x3.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-3x4.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-4x1.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-4x2.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-4x3.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-4x4.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temperature-id-1x1.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-1x2.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-1x3.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-1x4.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-2x1.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-2x2.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-2x3.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-2x4.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-3x1.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-3x2.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-3x3.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-3x4.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-4x1.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-4x2.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-4x3.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-4x4.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-ctype-id-1x1.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-1x2.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-1x3.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-1x4.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-2x1.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-2x2.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-2x3.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-2x4.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-3x1.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-3x2.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-3x3.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-3x4.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-4x1.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-4x2.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-4x3.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-4x4.label = ctype
thing-type.bambulab.printer.channel.ams-k-id-1x1.label = k
thing-type.bambulab.printer.channel.ams-k-id-1x2.label = k
thing-type.bambulab.printer.channel.ams-k-id-1x3.label = k
thing-type.bambulab.printer.channel.ams-k-id-1x4.label = k
thing-type.bambulab.printer.channel.ams-k-id-2x1.label = k
thing-type.bambulab.printer.channel.ams-k-id-2x2.label = k
thing-type.bambulab.printer.channel.ams-k-id-2x3.label = k
thing-type.bambulab.printer.channel.ams-k-id-2x4.label = k
thing-type.bambulab.printer.channel.ams-k-id-3x1.label = k
thing-type.bambulab.printer.channel.ams-k-id-3x2.label = k
thing-type.bambulab.printer.channel.ams-k-id-3x3.label = k
thing-type.bambulab.printer.channel.ams-k-id-3x4.label = k
thing-type.bambulab.printer.channel.ams-k-id-4x1.label = k
thing-type.bambulab.printer.channel.ams-k-id-4x2.label = k
thing-type.bambulab.printer.channel.ams-k-id-4x3.label = k
thing-type.bambulab.printer.channel.ams-k-id-4x4.label = k
thing-type.bambulab.printer.channel.ams-n-id-1x1.label = n
thing-type.bambulab.printer.channel.ams-n-id-1x2.label = n
thing-type.bambulab.printer.channel.ams-n-id-1x3.label = n
thing-type.bambulab.printer.channel.ams-n-id-1x4.label = n
thing-type.bambulab.printer.channel.ams-n-id-2x1.label = n
thing-type.bambulab.printer.channel.ams-n-id-2x2.label = n
thing-type.bambulab.printer.channel.ams-n-id-2x3.label = n
thing-type.bambulab.printer.channel.ams-n-id-2x4.label = n
thing-type.bambulab.printer.channel.ams-n-id-3x1.label = n
thing-type.bambulab.printer.channel.ams-n-id-3x2.label = n
thing-type.bambulab.printer.channel.ams-n-id-3x3.label = n
thing-type.bambulab.printer.channel.ams-n-id-3x4.label = n
thing-type.bambulab.printer.channel.ams-n-id-4x1.label = n
thing-type.bambulab.printer.channel.ams-n-id-4x2.label = n
thing-type.bambulab.printer.channel.ams-n-id-4x3.label = n
thing-type.bambulab.printer.channel.ams-n-id-4x4.label = n
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x1.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x2.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x3.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x4.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x1.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x2.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x3.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x4.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x1.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x2.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x3.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x4.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x1.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x2.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x3.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x4.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-4x4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x1.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x2.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x3.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x4.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x1.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x2.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x3.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x4.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x1.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x2.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x3.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x4.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x1.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x2.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x3.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x4.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-4x4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-remain-id-1x1.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-1x1.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-1x2.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-1x2.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-1x3.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-1x3.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-1x4.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-1x4.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-2x1.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-2x1.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-2x2.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-2x2.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-2x3.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-2x3.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-2x4.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-2x4.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-3x1.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-3x1.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-3x2.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-3x2.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-3x3.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-3x3.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-3x4.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-3x4.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-4x1.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-4x1.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-4x2.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-4x2.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-4x3.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-4x3.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-4x4.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-4x4.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-tag-uuid-id-1x1.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-1x2.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-1x3.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-1x4.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-2x1.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-2x2.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-2x3.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-2x4.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-3x1.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-3x2.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-3x3.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-3x4.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-4x1.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-4x2.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-4x3.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-4x4.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tray-diameter-id-1x1.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-1x2.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-1x3.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-1x4.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-2x1.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-2x2.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-2x3.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-2x4.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-3x1.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-3x2.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-3x3.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-3x4.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-4x1.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-4x2.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-4x3.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-4x4.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-id-name-id-1x1.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-1x2.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-1x3.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-1x4.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-2x1.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-2x2.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-2x3.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-2x4.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-3x1.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-3x2.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-3x3.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-3x4.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-4x1.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-4x2.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-4x3.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-4x4.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-1x1.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-1x2.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-1x3.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-1x4.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-2x1.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-2x2.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-2x3.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-2x4.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-3x1.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-3x2.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-3x3.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-3x4.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-4x1.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-4x2.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-4x3.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-4x4.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-1x1.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-1x2.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-1x3.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-1x4.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-2x1.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-2x2.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-2x3.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-2x4.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-3x1.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-3x2.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-3x3.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-3x4.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-4x1.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-4x2.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-4x3.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-4x4.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-temperature-id-1x1.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-1x2.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-1x3.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-1x4.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-2x1.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-2x2.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-2x3.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-2x4.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-3x1.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-3x2.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-3x3.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-3x4.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-4x1.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-4x2.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-4x3.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-4x4.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-time-id-1x1.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-1x2.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-1x3.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-1x4.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-2x1.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-2x2.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-2x3.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-2x4.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-3x1.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-3x2.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-3x3.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-3x4.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-4x1.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-4x2.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-4x3.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-4x4.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-weight-id-1x1.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-1x2.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-1x3.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-1x4.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-2x1.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-2x2.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-2x3.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-2x4.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-3x1.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-3x2.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-3x3.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-3x4.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-4x1.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-4x2.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-4x3.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-4x4.label = Tray Weight
# thing types config
thing-type.config.bambulab.ams-device.init.no-bridge = There is no bridge!
thing-type.config.bambulab.ams-device.init.bridge-wrong-type = Bridge has wrong type!
# thing types
thing-type.bambulab.ams.label = Bambu Lab AMS Device
thing-type.bambulab.ams.description = Represents a Bambu Lab AMS connected to a printer.
thing-type.bambulab.ams.channel.ams-bed-temp-type-id-1.label = Bed Temperature Type
thing-type.bambulab.ams.channel.ams-bed-temp-type-id-2.label = Bed Temperature Type
thing-type.bambulab.ams.channel.ams-bed-temp-type-id-3.label = Bed Temperature Type
thing-type.bambulab.ams.channel.ams-bed-temp-type-id-4.label = Bed Temperature Type
thing-type.bambulab.ams.channel.ams-bed-temperature-id-1.label = Bed Temperature
thing-type.bambulab.ams.channel.ams-bed-temperature-id-2.label = Bed Temperature
thing-type.bambulab.ams.channel.ams-bed-temperature-id-3.label = Bed Temperature
thing-type.bambulab.ams.channel.ams-bed-temperature-id-4.label = Bed Temperature
thing-type.bambulab.ams.channel.ams-ctype-id-1.label = ctype
thing-type.bambulab.ams.channel.ams-ctype-id-2.label = ctype
thing-type.bambulab.ams.channel.ams-ctype-id-3.label = ctype
thing-type.bambulab.ams.channel.ams-ctype-id-4.label = ctype
thing-type.bambulab.ams.channel.ams-k-id-1.label = k
thing-type.bambulab.ams.channel.ams-k-id-2.label = k
thing-type.bambulab.ams.channel.ams-k-id-3.label = k
thing-type.bambulab.ams.channel.ams-k-id-4.label = k
thing-type.bambulab.ams.channel.ams-n-id-1.label = n
thing-type.bambulab.ams.channel.ams-n-id-2.label = n
thing-type.bambulab.ams.channel.ams-n-id-3.label = n
thing-type.bambulab.ams.channel.ams-n-id-4.label = n
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-1.label = Nozzle Temperature Max
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-2.label = Nozzle Temperature Max
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-3.label = Nozzle Temperature Max
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-4.label = Nozzle Temperature Max
thing-type.bambulab.ams.channel.ams-nozzle-temperature-max-id-4.description = Maximum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-1.label = Nozzle Temperature Min
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-2.label = Nozzle Temperature Min
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-3.label = Nozzle Temperature Min
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-4.label = Nozzle Temperature Min
thing-type.bambulab.ams.channel.ams-nozzle-temperature-min-id-4.description = Minimum nozzle temperature for tray.
thing-type.bambulab.ams.channel.ams-remain-id-1.label = Remain
thing-type.bambulab.ams.channel.ams-remain-id-1.description = Amount of remaining filament.
thing-type.bambulab.ams.channel.ams-remain-id-2.label = Remain
thing-type.bambulab.ams.channel.ams-remain-id-2.description = Amount of remaining filament.
thing-type.bambulab.ams.channel.ams-remain-id-3.label = Remain
thing-type.bambulab.ams.channel.ams-remain-id-3.description = Amount of remaining filament.
thing-type.bambulab.ams.channel.ams-remain-id-4.label = Remain
thing-type.bambulab.ams.channel.ams-remain-id-4.description = Amount of remaining filament.
thing-type.bambulab.ams.channel.ams-tag-uuid-id-1.label = Tag UUID
thing-type.bambulab.ams.channel.ams-tag-uuid-id-2.label = Tag UUID
thing-type.bambulab.ams.channel.ams-tag-uuid-id-3.label = Tag UUID
thing-type.bambulab.ams.channel.ams-tag-uuid-id-4.label = Tag UUID
thing-type.bambulab.ams.channel.ams-tray-diameter-id-1.label = Tray Diameter
thing-type.bambulab.ams.channel.ams-tray-diameter-id-2.label = Tray Diameter
thing-type.bambulab.ams.channel.ams-tray-diameter-id-3.label = Tray Diameter
thing-type.bambulab.ams.channel.ams-tray-diameter-id-4.label = Tray Diameter
thing-type.bambulab.ams.channel.ams-tray-id-name-id-1.label = Tray ID Name
thing-type.bambulab.ams.channel.ams-tray-id-name-id-2.label = Tray ID Name
thing-type.bambulab.ams.channel.ams-tray-id-name-id-3.label = Tray ID Name
thing-type.bambulab.ams.channel.ams-tray-id-name-id-4.label = Tray ID Name
thing-type.bambulab.ams.channel.ams-tray-info-idx-id-1.label = Tray Info IDX Name
thing-type.bambulab.ams.channel.ams-tray-info-idx-id-2.label = Tray Info IDX Name
thing-type.bambulab.ams.channel.ams-tray-info-idx-id-3.label = Tray Info IDX Name
thing-type.bambulab.ams.channel.ams-tray-info-idx-id-4.label = Tray Info IDX Name
thing-type.bambulab.ams.channel.ams-tray-previous.label = Previously Loaded Tray
thing-type.bambulab.ams.channel.ams-tray-sub-brands-id-1.label = Tray Sub Brands
thing-type.bambulab.ams.channel.ams-tray-sub-brands-id-2.label = Tray Sub Brands
thing-type.bambulab.ams.channel.ams-tray-sub-brands-id-3.label = Tray Sub Brands
thing-type.bambulab.ams.channel.ams-tray-sub-brands-id-4.label = Tray Sub Brands
thing-type.bambulab.ams.channel.ams-tray-temperature-id-1.label = Tray Temperature
thing-type.bambulab.ams.channel.ams-tray-temperature-id-2.label = Tray Temperature
thing-type.bambulab.ams.channel.ams-tray-temperature-id-3.label = Tray Temperature
thing-type.bambulab.ams.channel.ams-tray-temperature-id-4.label = Tray Temperature
thing-type.bambulab.ams.channel.ams-tray-time-id-1.label = Tray Time
thing-type.bambulab.ams.channel.ams-tray-time-id-2.label = Tray Time
thing-type.bambulab.ams.channel.ams-tray-time-id-3.label = Tray Time
thing-type.bambulab.ams.channel.ams-tray-time-id-4.label = Tray Time
thing-type.bambulab.ams.channel.ams-tray-weight-id-1.label = Tray Weight
thing-type.bambulab.ams.channel.ams-tray-weight-id-2.label = Tray Weight
thing-type.bambulab.ams.channel.ams-tray-weight-id-3.label = Tray Weight
thing-type.bambulab.ams.channel.ams-tray-weight-id-4.label = Tray Weight
# thing types config
thing-type.config.bambulab.ams.number.label = AMS Number
thing-type.config.bambulab.ams.number.description = Order of the AMS
# thing types
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 types
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-0x0.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-0x1.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-0x2.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-0x3.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-1x0.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-2x0.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temp-type-id-3x0.label = Bed Temperature Type
thing-type.bambulab.printer.channel.ams-bed-temperature-id-0x0.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-0x1.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-0x2.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-0x3.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-1x0.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-2x0.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-bed-temperature-id-3x0.label = Bed Temperature
thing-type.bambulab.printer.channel.ams-ctype-id-0x0.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-0x1.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-0x2.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-0x3.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-1x0.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-2x0.label = ctype
thing-type.bambulab.printer.channel.ams-ctype-id-3x0.label = ctype
thing-type.bambulab.printer.channel.ams-k-id-0x0.label = k
thing-type.bambulab.printer.channel.ams-k-id-0x1.label = k
thing-type.bambulab.printer.channel.ams-k-id-0x2.label = k
thing-type.bambulab.printer.channel.ams-k-id-0x3.label = k
thing-type.bambulab.printer.channel.ams-k-id-1x0.label = k
thing-type.bambulab.printer.channel.ams-k-id-2x0.label = k
thing-type.bambulab.printer.channel.ams-k-id-3x0.label = k
thing-type.bambulab.printer.channel.ams-n-id-0x0.label = n
thing-type.bambulab.printer.channel.ams-n-id-0x1.label = n
thing-type.bambulab.printer.channel.ams-n-id-0x2.label = n
thing-type.bambulab.printer.channel.ams-n-id-0x3.label = n
thing-type.bambulab.printer.channel.ams-n-id-1x0.label = n
thing-type.bambulab.printer.channel.ams-n-id-2x0.label = n
thing-type.bambulab.printer.channel.ams-n-id-3x0.label = n
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x0.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x0.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x1.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x1.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x2.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x2.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x3.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-0x3.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x0.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-1x0.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x0.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-2x0.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x0.label = Nozzle Temperature Max
thing-type.bambulab.printer.channel.ams-nozzle-temperature-max-id-3x0.description = Maximum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x0.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x0.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x1.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x1.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x2.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x2.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x3.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-0x3.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x0.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-1x0.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x0.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-2x0.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x0.label = Nozzle Temperature Min
thing-type.bambulab.printer.channel.ams-nozzle-temperature-min-id-3x0.description = Minimum nozzle temperature for tray.
thing-type.bambulab.printer.channel.ams-remain-id-0x0.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-0x0.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-0x1.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-0x1.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-0x2.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-0x2.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-0x3.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-0x3.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-1x0.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-1x0.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-2x0.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-2x0.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-remain-id-3x0.label = Remain
thing-type.bambulab.printer.channel.ams-remain-id-3x0.description = Amount of remaining filament.
thing-type.bambulab.printer.channel.ams-tag-uuid-id-0x0.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-0x1.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-0x2.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-0x3.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-1x0.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-2x0.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tag-uuid-id-3x0.label = Tag UUID
thing-type.bambulab.printer.channel.ams-tray-diameter-id-0x0.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-0x1.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-0x2.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-0x3.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-1x0.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-2x0.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-diameter-id-3x0.label = Tray Diameter
thing-type.bambulab.printer.channel.ams-tray-id-name-id-0x0.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-0x1.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-0x2.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-0x3.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-1x0.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-2x0.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-id-name-id-3x0.label = Tray ID Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-0x0.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-0x1.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-0x2.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-0x3.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-1x0.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-2x0.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-info-idx-id-3x0.label = Tray Info IDX Name
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-0x0.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-0x1.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-0x2.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-0x3.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-1x0.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-2x0.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-sub-brands-id-3x0.label = Tray Sub Brands
thing-type.bambulab.printer.channel.ams-tray-temperature-id-0x0.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-0x1.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-0x2.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-0x3.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-1x0.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-2x0.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-temperature-id-3x0.label = Tray Temperature
thing-type.bambulab.printer.channel.ams-tray-time-id-0x0.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-0x1.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-0x2.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-0x3.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-1x0.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-2x0.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-time-id-3x0.label = Tray Time
thing-type.bambulab.printer.channel.ams-tray-weight-id-0x0.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-0x1.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-0x2.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-0x3.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-1x0.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-2x0.label = Tray Weight
thing-type.bambulab.printer.channel.ams-tray-weight-id-3x0.label = Tray Weight
# channel types
channel-type.bambulab.temperature.label = Temperature
@@ -191,8 +913,6 @@ 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
@@ -240,4 +960,5 @@ printer.handler.init.noHostname = Please pass hostname!
printer.handler.init.invalidHostname = Invalid hostname "{0}"!
printer.handler.init.cannotConnectCamera = Cannot connect to camera! {0}
printer.handler.init.noSerial = Please pass serial!
printer.handler.init.connectionLost = Connection lost! {0}
printer.handler.init.noAccessCode = Please pass access code!
@@ -0,0 +1,41 @@
<?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="ams-device">
<supported-bridge-type-refs>
<bridge-type-ref id="printer"/>
</supported-bridge-type-refs>
<label>Bambu Lab AMS Device</label>
<description>Represents a Bambu Lab AMS connected to a printer.</description>
<channel-groups>
<!-- each AMS contains exactly 4 trays -->
<!-- not all of them might be used at the same time -->
<!-- but still channels should exist for them -->
<channel-group id="ams-tray-1" typeId="ams-tray">
<label>AMS Tray #1</label>
</channel-group>
<channel-group id="ams-tray-2" typeId="ams-tray">
<label>AMS Tray #2</label>
</channel-group>
<channel-group id="ams-tray-3" typeId="ams-tray">
<label>AMS Tray #3</label>
</channel-group>
<channel-group id="ams-tray-4" typeId="ams-tray">
<label>AMS Tray #4</label>
</channel-group>
</channel-groups>
<representation-property>number</representation-property>
<config-description>
<parameter name="number" type="integer" required="true" min="1" max="4">
<label>AMS Number</label>
<description>Order of the AMS</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>
@@ -0,0 +1,65 @@
<?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-group-type id="ams-tray">
<label>AMS Tray</label>
<description>This channel group represents a single AMS tray.</description>
<channels>
<channel id="ams-tray-type" typeId="tray-type"/>
<channel id="ams-tray-color" typeId="tray-color"/>
<channel id="ams-nozzle-temperature-max" typeId="temperature-setpoint">
<label>Nozzle Temperature Max</label>
<description>Maximum nozzle temperature for tray.</description>
</channel>
<channel id="ams-nozzle-temperature-min" typeId="temperature-setpoint">
<label>Nozzle Temperature Min</label>
<description>Minimum nozzle temperature for tray.</description>
</channel>
<channel id="ams-remain" typeId="percent">
<label>Remain</label>
<description>Amount of remaining filament.</description>
</channel>
<channel id="ams-k" typeId="number-advanced">
<label>k</label>
</channel>
<channel id="ams-n" typeId="number-advanced">
<label>n</label>
</channel>
<channel id="ams-tag-uuid" typeId="string-advanced">
<label>Tag UUID</label>
</channel>
<channel id="ams-tray-id-name" typeId="string-advanced">
<label>Tray ID Name</label>
</channel>
<channel id="ams-tray-info-idx" typeId="string-advanced">
<label>Tray Info IDX Name</label>
</channel>
<channel id="ams-tray-sub-brands" typeId="string-advanced">
<label>Tray Sub Brands</label>
</channel>
<channel id="ams-tray-weight" typeId="number-advanced">
<label>Tray Weight</label>
</channel>
<channel id="ams-tray-diameter" typeId="number-advanced">
<label>Tray Diameter</label>
</channel>
<channel id="ams-tray-temperature" typeId="temperature-measurement-advanced">
<label>Tray Temperature</label>
</channel>
<channel id="ams-tray-time" typeId="number-advanced">
<label>Tray Time</label>
</channel>
<channel id="ams-bed-temp-type" typeId="string-advanced">
<label>Bed Temperature Type</label>
</channel>
<channel id="ams-bed-temperature" typeId="temperature-measurement-advanced">
<label>Bed Temperature</label>
</channel>
<channel id="ams-ctype" typeId="number-advanced">
<label>ctype</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -11,7 +11,17 @@
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
<state readOnly="true"/>
</channel-type>
<channel-type id="temperature-measurement-advanced" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="temperature-setpoint">
<item-type>Number:Temperature</item-type>
@@ -21,7 +31,7 @@
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
<state readOnly="true"/>
</channel-type>
<channel-type id="string">
<item-type>String</item-type>
@@ -32,6 +42,11 @@
<item-type>String</item-type>
<label>RW String Channel</label>
</channel-type>
<channel-type id="string-advanced" advanced="true">
<item-type>String</item-type>
<label>Advanced 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>
@@ -43,10 +58,20 @@
<label>Number Channel</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="time-min">
<item-type unitHint="min">Number:Time</item-type>
<label>Time (min)</label>
<state readOnly="true" pattern="%1$tHh:%1$tMm"/>
</channel-type>
<channel-type id="number-advanced" advanced="true">
<item-type>Number</item-type>
<label>Advanced 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%"/>
<state readOnly="true" min="0" max="100"/>
</channel-type>
<channel-type id="boolean">
<item-type>Switch</item-type>
@@ -86,21 +111,58 @@
</command>
</channel-type>
<channel-type id="on-off-command">
<!-- tray -->
<channel-type id="tray-type">
<item-type>String</item-type>
<label>ON/OFF Channel</label>
<category>Lightbulb</category>
<state>
<label>Tray Type</label>
<state readOnly="true">
<options>
<option value="ON">ON</option>
<option value="OFF">OFF</option>
<option value="PLA">PLA</option>
<option value="PETG">PETG</option>
<option value="ABS">ABS</option>
<option value="TPU">TPU</option>
<option value="ASA">ASA</option>
<option value="PA">PA</option>
<option value="PC">PC</option>
<option value="PVA">PVA</option>
<option value="HIPS">HIPS</option>
</options>
</state>
<command>
<options>
<option value="ON">ON</option>
<option value="OFF">OFF</option>
</options>
</command>
</channel-type>
<channel-type id="tray-color">
<item-type>Color</item-type>
<label>Tray Color</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="tray-loaded">
<item-type>String</item-type>
<label>Currently Loaded Tray</label>
<state readOnly="true">
<options>
<option value="EMPTY">No tray loaded</option>
<option value="VTRAY">External Spool</option>
<!-- AMS 1 -->
<option value="AMS_1_1">AMS #1 / Tray 1</option>
<option value="AMS_1_2">AMS #1 / Tray 2</option>
<option value="AMS_1_3">AMS #1 / Tray 3</option>
<option value="AMS_1_4">AMS #1 / Tray 4</option>
<!-- AMS 2 -->
<option value="AMS_2_1">AMS #2 / Tray 1</option>
<option value="AMS_2_2">AMS #2 / Tray 2</option>
<option value="AMS_2_3">AMS #2 / Tray 3</option>
<option value="AMS_2_4">AMS #2 / Tray 4</option>
<!-- AMS 3 -->
<option value="AMS_3_1">AMS #3 / Tray 1</option>
<option value="AMS_3_2">AMS #3 / Tray 2</option>
<option value="AMS_3_3">AMS #3 / Tray 3</option>
<option value="AMS_3_4">AMS #3 / Tray 4</option>
<!-- AMS 4 -->
<option value="AMS_4_1">AMS #4 / Tray 1</option>
<option value="AMS_4_2">AMS #4 / Tray 2</option>
<option value="AMS_4_3">AMS #4 / Tray 3</option>
<option value="AMS_4_4">AMS #4 / Tray 4</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>
@@ -4,11 +4,17 @@
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">
<bridge-type id="printer">
<label>Bambu Lab Printer</label>
<description>Represents a Bambu Lab 3D printer connected via MQTT</description>
<channels>
<channel id="command" typeId="string-rw">
<label>Command</label>
<description><![CDATA[
Send command to run. See <a href="https://github.com/openhab/openhab-addons/tree/main/bundles/org.openhab.binding.bambulab#sendcommand">Actions > sendCommand</a>
]]></description>
</channel>
<channel id="nozzle-temperature" typeId="temperature-measurement">
<label>Nozzle Temperature</label>
<description>Current temperature of the nozzle.</description>
@@ -37,7 +43,7 @@
<label>Print Progress</label>
<description>Percentage of the print completed.</description>
</channel>
<channel id="mc-remaining-time" typeId="number">
<channel id="mc-remaining-time" typeId="time-min">
<label>Remaining Print Time</label>
<description>Estimated time remaining for the print in seconds.</description>
</channel>
@@ -105,14 +111,73 @@
<channel id="camera-image" typeId="camera-image"/>
<!-- command channels -->
<channel id="led-chamber" typeId="on-off-command">
<channel id="led-chamber" typeId="boolean-rw">
<label>Chamber LED</label>
<description>Controls the LED lighting inside the printer chamber.</description>
</channel>
<channel id="led-work" typeId="on-off-command">
<channel id="led-work" typeId="boolean-rw">
<label>Work Area LED</label>
<description>Controls the LED lighting for the work area.</description>
</channel>
<!-- 🟩 AMS -->
<channel id="ams-tray-now" typeId="tray-loaded"/>
<channel id="ams-tray-previous" typeId="tray-loaded">
<label>Previously Loaded Tray</label>
</channel>
<!-- 🟥 AMS -->
<!-- 🟩 VTray -->
<channel id="vtray-tray-type" typeId="tray-type"/>
<channel id="vtray-tray-color" typeId="tray-color"/>
<channel id="vtray-nozzle-temperature-max" typeId="temperature-setpoint">
<label>Nozzle Temperature Max</label>
<description>Maximum nozzle temperature for tray.</description>
</channel>
<channel id="vtray-nozzle-temperature-min" typeId="temperature-setpoint">
<label>Nozzle Temperature Min</label>
<description>Minimum nozzle temperature for tray.</description>
</channel>
<channel id="vtray-remain" typeId="percent">
<label>Remain</label>
<description>Amount of remaining filament.</description>
</channel>
<channel id="vtray-k" typeId="number-advanced">
<label>k</label>
</channel>
<channel id="vtray-n" typeId="number-advanced">
<label>n</label>
</channel>
<channel id="vtray-tag-uuid" typeId="string-advanced">
<label>Tag UUID</label>
</channel>
<channel id="vtray-tray-id-name" typeId="string-advanced">
<label>Tray ID Name</label>
</channel>
<channel id="vtray-tray-info-idx" typeId="string-advanced">
<label>Tray Info IDX Name</label>
</channel>
<channel id="vtray-tray-sub-brands" typeId="string-advanced">
<label>Tray Sub Brands</label>
</channel>
<channel id="vtray-tray-weight" typeId="number-advanced">
<label>Tray Weight</label>
</channel>
<channel id="vtray-tray-diameter" typeId="number-advanced">
<label>Tray Diameter</label>
</channel>
<channel id="vtray-tray-temperature" typeId="temperature-measurement-advanced">
<label>Tray Temperature</label>
</channel>
<channel id="vtray-tray-time" typeId="number-advanced">
<label>Tray Time</label>
</channel>
<channel id="vtray-bed-temp-type" typeId="string-advanced">
<label>Bed Temperature Type</label>
</channel>
<channel id="vtray-bed-temperature" typeId="temperature-measurement-advanced">
<label>Bed Temperature</label>
</channel>
<!-- 🟥 VTray -->
</channels>
<representation-property>serial</representation-property>
@@ -157,6 +222,18 @@
<description>`bblp` for local mode or your Bambu Lab user (starts with `u_`).</description>
<advanced>true</advanced>
</parameter>
<parameter name="reconnectTime" type="text" min="1">
<label>Reconnect Time</label>
<description>Time in seconds to wait before reconnect.</description>
<default>300</default>
<advanced>true</advanced>
</parameter>
<parameter name="reconnectMax" type="text" min="0">
<label>Max Reconnections</label>
<description>Max times to try reconnections.</description>
<default>5</default>
<advanced>true</advanced>
</parameter>
<!-- 🟥Common -->
<!-- 🟩MQTT -->
<parameter name="scheme" type="text" required="false" groupName="mqtt">
@@ -187,6 +264,6 @@
</parameter>
<!-- 🟥CAMERA -->
</config-description>
</thing-type>
</bridge-type>
</thing:thing-descriptions>
@@ -12,9 +12,7 @@
*/
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 org.assertj.core.api.Assertions.*;
import static pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand.LedMode.*;
import static pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand.LedNode.*;
@@ -24,45 +22,20 @@ 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 org.reflections.Reflections;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.AmsControlCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.AmsFilamentSettingCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.AmsUserSettingCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.ChangeFilamentCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.Command;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.InfoCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.IpCamRecordCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.LedControlCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintSpeedCommand;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PushingCommand;
import pl.grzeslowski.jbambuapi.mqtt.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));
}
class CommandParserTest {
@Test
@DisplayName("should correctly parse GCodeLineCommand")
@@ -77,10 +50,10 @@ class PrinterActionsTest {
M104 S200 ; Set hotend temperature to 200°C""";
// when
printerActions.sendCommand(command);
var parsedCommand = CommandParser.parseCommand(command);
// then
verify(requireNonNull(printerHandler)).sendCommand(new PrinterClient.Channel.GCodeLineCommand(
assertThat(parsedCommand).isEqualTo(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",
@@ -95,7 +68,7 @@ class PrinterActionsTest {
var command = "GCodeLine:123";
// when
ThrowableAssert.ThrowingCallable when = () -> printerActions.sendCommand(command);
ThrowableAssert.ThrowingCallable when = () -> CommandParser.parseCommand(command);
// then
assertThatThrownBy(when)//
@@ -105,29 +78,30 @@ class PrinterActionsTest {
@ParameterizedTest(name = "{index}: should {0}")
@MethodSource
void shouldRunCommand(String command, Command expectedCommand) {
void shouldRunCommand(String command, PrinterClient.Channel.Command expectedCommand) {
// when
printerActions.sendCommand(command);
var parsedCommand = CommandParser.parseCommand(command);
// then
verify(requireNonNull(printerHandler)).sendCommand(expectedCommand);
assertThat(parsedCommand).isEqualTo(expectedCommand);
}
static Stream<Arguments> shouldRunCommand() {
var infoCommandStream = Arrays.stream(InfoCommand.values())//
var infoCommandStream = Arrays.stream(PrinterClient.Channel.InfoCommand.values())//
.map(value -> Arguments.of("Info:" + value.name(), value));
var pushingCommandStream = stream(Arguments.of("Pushing:11:22", new PushingCommand(11, 22)));
var pushingCommandStream = stream(
Arguments.of("Pushing:11:22", new PrinterClient.Channel.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)));
Arguments.of("ChangeFilament:11:22:33", new PrinterClient.Channel.ChangeFilamentCommand(11, 22, 33)));
var amsUserSettingCommandStream = stream(Arguments.of("AmsUserSetting:11:tRuE:FaLsE",
new PrinterClient.Channel.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())//
new PrinterClient.Channel.AmsFilamentSettingCommand(11, 22, "s3", "s4", 55, 66, "s7")));
var amsControlCommandStream = Arrays.stream(PrinterClient.Channel.AmsControlCommand.values())//
.map(value -> Arguments.of("AmsControl:" + value.name(), value));
var printSpeedCommandStream = Arrays.stream(PrintSpeedCommand.values())//
var printSpeedCommandStream = Arrays.stream(PrinterClient.Channel.PrintSpeedCommand.values())//
.map(value -> Arguments.of("PrintSpeed:" + value.name(), value));
var gCodeFileCommandStream = stream(
Arguments.of("GCodeFile:s1", new PrinterClient.Channel.GCodeFileCommand("s1")));
@@ -138,17 +112,17 @@ class PrinterActionsTest {
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)), //
new PrinterClient.Channel.LedControlCommand(CHAMBER_LIGHT, ON, null, null, null, null)), //
Arguments.of("LedControl:WORK_LIGHT:OFF",
new LedControlCommand(WORK_LIGHT, OFF, null, null, null, null)), //
new PrinterClient.Channel.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))//
new PrinterClient.Channel.LedControlCommand(CHAMBER_LIGHT, FLASHING, 11, 22, 33, 44))//
);
var systemCommandStream = Arrays.stream(SystemCommand.values())//
var systemCommandStream = Arrays.stream(PrinterClient.Channel.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))//
Arguments.of("IpCamRecord:tRue", new PrinterClient.Channel.IpCamRecordCommand(true)), //
Arguments.of("IpCamRecord:fAlSe", new PrinterClient.Channel.IpCamRecordCommand(false))//
);
var ipCamTimelapsCommandStream = stream(//
Arguments.of("IpCamTimelaps:tRue", new PrinterClient.Channel.IpCamTimelapsCommand(true)), //
@@ -173,4 +147,32 @@ class PrinterActionsTest {
static <T> Stream<T> stream(T... values) {
return Stream.of(values);
}
@ParameterizedTest(name = "{index}: should support command {0}")
@MethodSource
void shouldSupportCommand(String command) {
// when
ThrowableAssert.ThrowingCallable when = () -> CommandParser.parseCommand(command);
// then
assertThatThrownBy(when)//
.message()//
// if an error message starts with "Unknown..." it means that the giant if statement did not cover
// command.
//
// it might happen if JBambuApi library adds a new subclass of Command and addon developers would not
// add it to PrinterActions.
//
// it might happen that there will be some other IllegalArgumentException (like not enough params) would
// be thrown, but none of them starts with "Unknown...".
.doesNotStartWith("Unknown command name: " + command);
}
static Stream<Arguments> shouldSupportCommand() {
return new Reflections(PrinterClient.Channel.Command.class)//
.getSubTypesOf(PrinterClient.Channel.Command.class)//
.stream()//
.map(Class::getSimpleName)//
.map(command -> command.replace("Command", "")).map(Arguments::of);
}
}
@@ -34,10 +34,11 @@ 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.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.Command;
import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintSpeedCommand;
/**
@@ -47,16 +48,16 @@ import pl.grzeslowski.jbambuapi.mqtt.PrinterClient.Channel.PrintSpeedCommand;
@NonNullByDefault
class PrinterHandlerTest {
@Spy
PrinterHandler printerHandler = new PrinterHandler(mock(Thing.class));
PrinterHandler printerHandler = new PrinterHandler(mock(Bridge.class));
@BeforeEach
void setUp() {
lenient().doNothing().when(printerHandler).sendCommand(any(PrinterClient.Channel.Command.class));
lenient().doNothing().when(printerHandler).sendCommand(any(Command.class));
}
@ParameterizedTest(name = "Should handle {0} command for {1} channel and send {2}")
@MethodSource
public void testSendLightCommand(OnOffType command, Channel channel, PrinterClient.Channel.Command sendCommand) {
public void testSendLightCommand(OnOffType command, Channel channel, Command sendCommand) {
// Given
var channelUID = new ChannelUID("bambulab:printer:test:" + channel);
@@ -116,7 +117,8 @@ class PrinterHandlerTest {
printerHandler.handleCommand(channelUID, OnOffType.ON);
// Then
verify(printerHandler, never()).sendCommand(any());
verify(printerHandler, never()).sendCommand(any(Command.class));
verify(printerHandler, never()).sendCommand(anyString());
}
static Stream<Arguments> notImplementedCommands() {
@@ -0,0 +1,522 @@
/*
* 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.assertj.core.api.Assertions.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.Units.DECIBEL_MILLIWATTS;
import static org.openhab.core.types.UnDefType.UNDEF;
import static tech.units.indriya.unit.Units.PERCENT;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
class StateParserHelperTest {
@Test
@DisplayName("Given a valid temperature string, when parseTemperatureType is called, then it returns QuantityType with CELSIUS unit")
void testParseTemperatureTypeConvertsValidStringToQuantityType() {
// Given
var temperatureString = "23.5";
// When
var result = StateParserHelper.parseTemperatureType(temperatureString);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(QuantityType.class);
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(23.5);
assertThat(quantityType.getUnit()).isEqualTo(CELSIUS);
}
@Test
@DisplayName("Given a valid Number, when parseDecimalType is called, then it returns DecimalType")
void testParseDecimalTypeConvertsValidNumberToDecimalType() {
// Given
var number = 42.5;
// When
var result = StateParserHelper.parseDecimalType(number);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(DecimalType.class);
assertThat(((DecimalType) result.get()).doubleValue()).isEqualTo(42.5);
}
@Test
@DisplayName("Given a valid numeric string, when parseDecimalType is called, then it returns DecimalType")
void testParseDecimalTypeConvertsValidStringToDecimalType() {
// Given
var decimalString = "123.45";
// When
var result = StateParserHelper.parseDecimalType(decimalString);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(DecimalType.class);
assertThat(((DecimalType) result.get()).doubleValue()).isEqualTo(123.45);
}
@Test
@DisplayName("Given a non-null string, when parseStringType is called, then it returns StringType")
void testParseStringTypeConvertsNonNullStringToStringType() {
// Given
var inputString = "test string";
// When
var result = StateParserHelper.parseStringType(inputString);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(StringType.class);
assertThat(result.get().toString()).isEqualTo("test string");
}
@Test
@DisplayName("Given a valid dBm pattern string, when parseWifiChannel is called, then it returns QuantityType with DECIBEL_MILLIWATTS unit")
void testParseWifiChannelConvertsValidDbmPatternToQuantityType() {
// Given
var wifiString = "-75dBm";
// When
var result = StateParserHelper.parseWifiChannel(wifiString);
// Then
assertThat(result).isInstanceOf(QuantityType.class);
var quantityType = (QuantityType<?>) result;
assertThat(quantityType.intValue()).isEqualTo(-75);
assertThat(quantityType.getUnit()).isEqualTo(DECIBEL_MILLIWATTS);
}
@Test
@DisplayName("Given a valid 6-character hex string, when parseColor is called, then it returns HSB color State")
void testParseColorConvertsValidHexStringToHsbColor() {
// Given
var colorHex = "FF0000"; // Red in RGB
// When
var result = StateParserHelper.parseColor(colorHex);
// Then
assertThat(result).isNotEqualTo(UNDEF);
// HSB for pure red should have H=0, S=100, B=100
var hsbValues = result.toString().split(",");
assertThat(Double.parseDouble(hsbValues[0])).isCloseTo(0.0, within(0.1));
assertThat(Double.parseDouble(hsbValues[1])).isCloseTo(100.0, within(0.1));
assertThat(Double.parseDouble(hsbValues[2])).isCloseTo(100.0, within(0.1));
}
@Test
@DisplayName("Given a null input, when parseTemperatureType is called, then it returns empty Optional")
void testParseTemperatureTypeReturnsEmptyOptionalForNullInput() {
// Given
String temperatureString = null;
// When
var result = StateParserHelper.parseTemperatureType(temperatureString);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a non-numeric string, when parseTemperatureType is called, then it returns empty Optional")
void testParseTemperatureTypeReturnsEmptyOptionalForNonNumericString() {
// Given
var temperatureString = "not-a-number";
// When
var result = StateParserHelper.parseTemperatureType(temperatureString);
// Then
assertThat(result).contains(UNDEF);
}
@Test
@DisplayName("Given a null Number, when parseDecimalType is called, then it returns empty Optional")
void testParseDecimalTypeReturnsEmptyOptionalForNullNumber() {
// Given
Number number = null;
// When
var result = StateParserHelper.parseDecimalType(number);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a null String, when parseDecimalType is called, then it returns empty Optional")
void testParseDecimalTypeReturnsEmptyOptionalForNullString() {
// Given
String decimalString = null;
// When
var result = StateParserHelper.parseDecimalType(decimalString);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a non-numeric String, when parseDecimalType is called, then it returns empty Optional")
void testParseDecimalTypeReturnsEmptyOptionalForNonNumericString() {
// Given
var decimalString = "not-a-decimal";
// When
var result = StateParserHelper.parseDecimalType(decimalString);
// Then
assertThat(result).contains(UNDEF);
}
@Test
@DisplayName("Given a null String, when parseStringType is called, then it returns empty Optional")
void testParseStringTypeReturnsEmptyOptionalForNullString() {
// Given
String inputString = null;
// When
var result = StateParserHelper.parseStringType(inputString);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a valid numeric value, when parsePercentType is called, then it returns Optional with QuantityType")
public void testValidNumericReturnsQuantityTypeWithPercentUnit() {
// Given
var numericValue = 75;
// When
var result = StateParserHelper.parsePercentType(numericValue);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(QuantityType.class);
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
assertThat(quantityType.doubleValue()).isEqualTo(75.0);
}
@Test
@DisplayName("Given an integer value, when parsePercentType is called, then it returns correct QuantityType")
public void testIntegerValuesAreCorrectlyParsed() {
// Given
var integerValue = 100;
// When
var result = StateParserHelper.parsePercentType(integerValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(100.0);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Given a decimal value, when parsePercentType is called, then it returns correct QuantityType")
public void testDecimalValuesAreCorrectlyParsed() {
// Given
var decimalValue = 50.5;
// When
var result = StateParserHelper.parsePercentType(decimalValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(50.5);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Given a zero value, when parsePercentType is called, then it returns QuantityType with zero value")
public void testZeroValueIsCorrectlyParsed2() {
// Given
var zeroValue = 0;
// When
var result = StateParserHelper.parsePercentType(zeroValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isZero();
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Given a negative value, when parsePercentType is called, then it returns QuantityType with negative value")
public void testNegativeValuesAreCorrectlyParsed2() {
// Given
var negativeValue = -10;
// When
var result = StateParserHelper.parsePercentType(negativeValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(-10.0);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Given a null input, when parsePercentType is called, then it returns Optional.empty")
public void testNullInputReturnsEmptyOptional2() {
// Given
Number nullValue = null;
// When
var result = StateParserHelper.parsePercentType(nullValue);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given an empty string input, when parsePercentType is called, then it returns Optional.empty")
public void testEmptyStringInputReturnsEmptyOptional2() {
// This test is not applicable as the method only accepts Number objects
// The method signature is: parsePercentType(@Nullable Number percent)
// Empty strings would be handled by a different method before this one is called
// For completeness, we'll verify that null returns empty
// Given
Number nullValue = null;
// When
var result = StateParserHelper.parsePercentType(nullValue);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a non-numeric string input, when parsePercentType is called, then it returns Optional.empty")
public void testNonNumericStringReturnsEmptyOptional2() {
// This test is not applicable as the method only accepts Number objects
// The method signature is: parsePercentType(@Nullable Number percent)
// Non-numeric strings would be handled by a different method before this one is called
// For completeness, we'll verify that null returns empty
// Given
Number nullValue = null;
// When
var result = StateParserHelper.parsePercentType(nullValue);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Given a very large number, when parsePercentType is called, then it returns correct QuantityType")
public void testVeryLargeNumbersAreHandledCorrectly2() {
// Given
var largeValue = Double.MAX_VALUE / 2;
// When
var result = StateParserHelper.parsePercentType(largeValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(largeValue);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Given a very small number, when parsePercentType is called, then it returns correct QuantityType")
public void testVerySmallNumbersAreHandledCorrectly2() {
// Given
var smallValue = Double.MIN_VALUE;
// When
var result = StateParserHelper.parsePercentType(smallValue);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(smallValue);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Valid numeric string returns Optional containing QuantityType with PERCENT unit")
public void testValidNumericStringReturnsQuantityTypeWithPercentUnit() {
// Given
var percentString = "75";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isInstanceOf(QuantityType.class);
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
assertThat(quantityType.doubleValue()).isEqualTo(75.0);
}
@Test
@DisplayName("Integer string values like '100' are correctly parsed")
public void testIntegerStringValuesAreCorrectlyParsed() {
// Given
var percentString = "100";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(100.0);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Decimal string values like '50.5' are correctly parsed")
public void testDecimalStringValuesAreCorrectlyParsed() {
// Given
var percentString = "50.5";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(50.5);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Zero value '0' is correctly parsed")
public void testZeroValueIsCorrectlyParsed() {
// Given
var percentString = "0";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(0.0);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Negative values like '-10' are correctly parsed")
public void testNegativeValuesAreCorrectlyParsed() {
// Given
var percentString = "-10";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(-10.0);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Null input returns Optional.empty()")
public void testNullInputReturnsEmptyOptional() {
// Given
String percentString = null;
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Empty string input throws NumberFormatException and returns Optional.empty()")
public void testEmptyStringInputReturnsEmptyOptional() {
// Given
var percentString = "";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).contains(UNDEF);
}
@Test
@DisplayName("Non-numeric string like 'abc' throws NumberFormatException and returns Optional.empty()")
public void testNonNumericStringReturnsEmptyOptional() {
// Given
var percentString = "abc";
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).contains(UNDEF);
}
@Test
@DisplayName("Very large numbers near Double.MAX_VALUE are handled correctly")
public void testVeryLargeNumbersAreHandledCorrectly() {
// Given
var percentString = String.valueOf(Double.MAX_VALUE);
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(Double.MAX_VALUE);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
@Test
@DisplayName("Very small numbers near Double.MIN_VALUE are handled correctly")
public void testVerySmallNumbersAreHandledCorrectly() {
// Given
var percentString = String.valueOf(Double.MIN_VALUE);
// When
var result = StateParserHelper.parsePercentType(percentString);
// Then
assertThat(result).isPresent();
var quantityType = (QuantityType<?>) result.get();
assertThat(quantityType.doubleValue()).isEqualTo(Double.MIN_VALUE);
assertThat(quantityType.getUnit()).isEqualTo(PERCENT);
}
}
@@ -0,0 +1,173 @@
/*
* 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.assertj.core.api.Assertions.assertThat;
import static org.openhab.core.types.UnDefType.UNDEF;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.library.types.StringType;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
class TrayHelperTest {
@Test
@DisplayName("String '255' returns EMPTY state")
public void testString255ReturnsEmpty() {
// Given
var tray = "255";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isInstanceOf(StringType.class);
assertThat(result.toString()).isEqualTo("EMPTY");
}
@Test
@DisplayName("String '254' returns VTRAY state")
public void testString254ReturnsVtray() {
// Given
var tray = "254";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isInstanceOf(StringType.class);
assertThat(result.toString()).isEqualTo("VTRAY");
}
@ParameterizedTest(name = "{index}: should parse {0} as {1}")
@MethodSource
public void happyPath(int tray, String expected) {
// When
var result = TrayHelper.updateTrayLoaded(tray + "");
// Then
assertThat(result.toString()).isEqualTo(expected);
}
static Stream<Arguments> happyPath() {
return Stream.of(
// AMS 1
Arguments.of(0, "AMS_1_1"), //
Arguments.of(1, "AMS_1_2"), //
Arguments.of(2, "AMS_1_3"), //
Arguments.of(3, "AMS_1_4"), //
// AMS 2
Arguments.of(0 + 4, "AMS_2_1"), //
Arguments.of(1 + 4, "AMS_2_2"), //
Arguments.of(2 + 4, "AMS_2_3"), //
Arguments.of(3 + 4, "AMS_2_4"), //
// AMS 3
Arguments.of(0 + 8, "AMS_3_1"), //
Arguments.of(1 + 8, "AMS_3_2"), //
Arguments.of(2 + 8, "AMS_3_3"), //
Arguments.of(3 + 8, "AMS_3_4"), //
// AMS 4
Arguments.of(0 + 12, "AMS_4_1"), //
Arguments.of(1 + 12, "AMS_4_2"), //
Arguments.of(2 + 12, "AMS_4_3"), //
Arguments.of(3 + 12, "AMS_4_4"));
}
@Test
@DisplayName("Null input returns UNDEF")
public void testNullInputReturnsUndef() {
// Given
String tray = null;
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isEqualTo(UNDEF);
}
@Test
@DisplayName("Non-numeric string returns UNDEF")
public void testNonNumericStringReturnsUndef() {
// Given
var tray = "not-a-number";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isEqualTo(UNDEF);
}
@Test
@DisplayName("Empty string returns UNDEF")
public void testEmptyStringReturnsUndef() {
// Given
var tray = "";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isEqualTo(UNDEF);
}
@Test
@DisplayName("Negative numbers are processed as undef")
public void negatives() {
// Given
var tray = "-5";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isEqualTo(UNDEF);
}
@Test
@DisplayName("Exceeding maximum tray value returns UNDEF with warning")
public void testExceedingMaxValueReturnsUndefWithWarning() {
// Given
var tray = (TrayHelper.MAX_TRAY_VALUE + 1) + "";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result).isEqualTo(UNDEF);
}
@Test
@DisplayName("Value for `MAX_TRAY_VALUE` should return `AMS_4_4`")
public void testForMaxTray() {
// Given
var tray = TrayHelper.MAX_TRAY_VALUE + "";
// When
var result = TrayHelper.updateTrayLoaded(tray);
// Then
assertThat(result.toString()).isEqualTo("AMS_4_4");
}
}
@@ -0,0 +1,130 @@
/*
* 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.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.binding.bambulab.internal.BambuLabBindingConstants.AmsChannel.TrayType;
/**
* @author Martin Grześlowski - Initial contribution
*/
@NonNullByDefault
class TrayTypeTest {
@Test
@DisplayName("Should find ABS when lowercase 'abs' is passed")
public void testSuccessfullyFindsAbsWhenLowercaseAbsIsPassed() {
// Given
var input = "abs";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isEqualTo(TrayType.ABS);
}
@Test
@DisplayName("Should find TPU when uppercase 'TPU' is passed")
public void testSuccessfullyFindsTpuWhenUppercaseTpuIsPassed() {
// Given
var input = "TPU";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isEqualTo(TrayType.TPU);
}
@Test
@DisplayName("Should return empty Optional when input doesn't match any TrayType")
public void testReturnsEmptyOptionalWhenInputDoesntMatchAnyTrayType() {
// Given
var input = "UNKNOWN_MATERIAL";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Should handle empty string input")
public void testHandlesEmptyStringInput() {
// Given
var input = "";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isEmpty();
}
@Test
@DisplayName("Should handle mixed case inputs like 'PeTg' correctly")
public void testHandlesMixedCaseInputsCorrectly() {
// Given
var input = "PeTg";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isPresent();
assertThat(result.get()).isEqualTo(TrayType.PETG);
}
@Test
@DisplayName("Should handle inputs with leading/trailing whitespace")
public void testHandlesInputsWithLeadingAndTrailingWhitespace() {
// Given
var input = " ABS ";
// When
var result = TrayType.findTrayType(input);
// Then
assertThat(result).isEmpty();
}
@ParameterizedTest(name = "{index}: should parse {0} to {1}")
@MethodSource
void testPerformsCaseInsensitiveComparisonForAllEnumValues(String given, TrayType trayType) {
assertThat(TrayType.findTrayType(given))//
.isPresent()//
.hasValueSatisfying(type -> assertThat(type).isEqualTo(trayType));
}
static Stream<Arguments> testPerformsCaseInsensitiveComparisonForAllEnumValues() {
return Arrays.stream(TrayType.values())//
.flatMap(trayType -> Stream.of(//
Arguments.of(trayType.name().toUpperCase(), trayType), //
Arguments.of(trayType.name().toLowerCase(), trayType), //
Arguments.of(trayType.name().charAt(0) + trayType.name().substring(1).toLowerCase(),
trayType)));
}
}