diff --git a/bundles/org.openhab.binding.bambulab/README.md b/bundles/org.openhab.binding.bambulab/README.md index b21f2e035b..5781e14b7b 100644 --- a/bundles/org.openhab.binding.bambulab/README.md +++ b/bundles/org.openhab.binding.bambulab/README.md @@ -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 | diff --git a/bundles/org.openhab.binding.bambulab/pom.xml b/bundles/org.openhab.binding.bambulab/pom.xml index 15f9bd3014..dec64ecca0 100644 --- a/bundles/org.openhab.binding.bambulab/pom.xml +++ b/bundles/org.openhab.binding.bambulab/pom.xml @@ -18,7 +18,7 @@ pl.grzeslowski JBambuAPI - 1.1.0 + 1.3.0 org.eclipse.paho @@ -34,6 +34,7 @@ org.slf4j slf4j-api 2.0.16 + provided @@ -49,6 +50,12 @@ 5.11.0 test + + org.reflections + reflections + 0.10.2 + test + diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceConfiguration.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceConfiguration.java new file mode 100644 index 0000000000..5dce32753d --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceConfiguration.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceHandlerFactory.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceHandlerFactory.java new file mode 100644 index 0000000000..4b1ba35ced --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/AmsDeviceHandlerFactory.java @@ -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 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); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuHandler.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuHandler.java index 054c6f3a4f..88c928780f 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuHandler.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuHandler.java @@ -59,7 +59,7 @@ public interface BambuHandler { /** * Returns current scheduler - * + * * @return scheduler */ ScheduledExecutorService getScheduler(); diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java index b736ffc192..3c1e0b7bd1 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabBindingConstants.java @@ -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 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; + } + } + } } diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java index 62f05f625c..9b883454a4 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/BambuLabHandlerFactory.java @@ -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 SUPPORTED_THING_TYPES_UIDS = Set.of(PRINTER_THING_TYPE); + private static final Set 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; diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/CommandParser.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/CommandParser.java new file mode 100644 index 0000000000..927ca038c9 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/CommandParser.java @@ -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])); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java index ebe0b20729..33b49a6830 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterActions.java @@ -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) { diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java index 46b02ad252..2c72cb346a 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterConfiguration.java @@ -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); } } diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java index dcdd540c86..ebea8281dd 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/PrinterHandler.java @@ -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 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() : " 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 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(); + } } diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/StateParserHelper.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/StateParserHelper.java new file mode 100644 index 0000000000..515c6717a2 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/StateParserHelper.java @@ -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 parseTimeMinutes(@Nullable Integer integer) { + return Optional.ofNullable(integer)// + .map(time -> new QuantityType<>(time, MINUTE)); + } + + public static Optional 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 parseDecimalType(@Nullable Number number) { + return Optional.ofNullable(number).map(DecimalType::new); + } + + public static Optional 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 parsePercentType(@Nullable Number percent) { + return Optional.ofNullable(percent).map(d -> new QuantityType<>(d, PERCENT)); + } + + public static Optional 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 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 undef() { + return Optional.of(UNDEF); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/TrayHelper.java b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/TrayHelper.java new file mode 100644 index 0000000000..14a871dbfd --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/java/org/openhab/binding/bambulab/internal/TrayHelper.java @@ -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)); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties index 0f8e099e8a..8d6076ee1c 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/i18n/bambulab.properties @@ -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 Actions > sendCommand 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! diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/ams-device-type.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/ams-device-type.xml new file mode 100644 index 0000000000..a329268f4c --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/ams-device-type.xml @@ -0,0 +1,41 @@ + + + + + + + + + Represents a Bambu Lab AMS connected to a printer. + + + + + + + + + + + + + + + + + + + + number + + + + Order of the AMS + + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-group-types.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-group-types.xml new file mode 100644 index 0000000000..e7cf258f36 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-group-types.xml @@ -0,0 +1,65 @@ + + + + + This channel group represents a single AMS tray. + + + + + + Maximum nozzle temperature for tray. + + + + Minimum nozzle temperature for tray. + + + + Amount of remaining filament. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml index 44faac80cd..662258f349 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/channel-types.xml @@ -11,7 +11,17 @@ Measurement Temperature - + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + Number:Temperature @@ -21,7 +31,7 @@ Setpoint Temperature - + String @@ -32,6 +42,11 @@ String + + String + + + Number:Power @@ -43,10 +58,20 @@ + + Number:Time + + + + + Number + + + Number:Dimensionless - + Switch @@ -86,21 +111,58 @@ - + + String - - Lightbulb - + + - - + + + + + + + + + - - - - - - + + Color + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml index 31beff2d50..d8fb5028e9 100644 --- a/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.bambulab/src/main/resources/OH-INF/thing/thing-types.xml @@ -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"> - + Represents a Bambu Lab 3D printer connected via MQTT + + + Actions > sendCommand + ]]> + Current temperature of the nozzle. @@ -37,7 +43,7 @@ Percentage of the print completed. - + Estimated time remaining for the print in seconds. @@ -105,14 +111,73 @@ - + Controls the LED lighting inside the printer chamber. - + Controls the LED lighting for the work area. + + + + + + + + + + + + + Maximum nozzle temperature for tray. + + + + Minimum nozzle temperature for tray. + + + + Amount of remaining filament. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + serial @@ -157,6 +222,18 @@ `bblp` for local mode or your Bambu Lab user (starts with `u_`). true + + + Time in seconds to wait before reconnect. + 300 + true + + + + Max times to try reconnections. + 5 + true + @@ -187,6 +264,6 @@ - + diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/CommandParserTest.java similarity index 61% rename from bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java rename to bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/CommandParserTest.java index 51ba7e29a0..5e6bf1148b 100644 --- a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterActionsTest.java +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/CommandParserTest.java @@ -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 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 Stream 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 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); + } } diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java index c8e6689a1c..ae542cf33b 100644 --- a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/PrinterHandlerTest.java @@ -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 notImplementedCommands() { diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/StateParserHelperTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/StateParserHelperTest.java new file mode 100644 index 0000000000..279e34586d --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/StateParserHelperTest.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayHelperTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayHelperTest.java new file mode 100644 index 0000000000..8daec36d30 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayHelperTest.java @@ -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 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"); + } +} diff --git a/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayTypeTest.java b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayTypeTest.java new file mode 100644 index 0000000000..6ccce366a8 --- /dev/null +++ b/bundles/org.openhab.binding.bambulab/src/test/java/org/openhab/binding/bambulab/internal/TrayTypeTest.java @@ -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 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))); + } +}