diff --git a/CODEOWNERS b/CODEOWNERS
index b75c95be6..51771a55b 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -169,6 +169,7 @@
/bundles/org.openhab.binding.loxone/ @ppieczul
/bundles/org.openhab.binding.luftdateninfo/ @weymann
/bundles/org.openhab.binding.lutron/ @actong @bobadair
+/bundles/org.openhab.binding.luxom/ @jesperskriasoft
/bundles/org.openhab.binding.luxtronikheatpump/ @sgiehl
/bundles/org.openhab.binding.magentatv/ @markus7017
/bundles/org.openhab.binding.mail/ @openhab/add-ons-maintainers
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 9bd4c43c6..2a983e6dc 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -836,6 +836,11 @@
org.openhab.binding.lutron${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.luxom
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.luxtronikheatpump
diff --git a/bundles/org.openhab.binding.luxom/NOTICE b/bundles/org.openhab.binding.luxom/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.luxom/README.md b/bundles/org.openhab.binding.luxom/README.md
new file mode 100644
index 000000000..56a1a1d7f
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/README.md
@@ -0,0 +1,98 @@
+# Luxom Binding
+
+This binding integrates with a https://luxom.io/ based system through a Luxom IP interface module.
+The binding has been tested with the DS65L IP interface, but it's not an official binding by Luxom.
+
+The API implementation is based on the following documentation:
+
+* https://old.luxom.io/uploads/ppfiles/27/LUXOM_ASCII.pdf
+* https://old.luxom.io/uploads/ppfiles/28/LUXOM_ASCII_extended.pdf
+
+## Supported Things
+
+This binding currently supports the following thing types:
+
+* **ipbridge** - The Lutron main repeater/processor/hub
+* **dimmer** - Light dimmer
+* **switch** - Switch or relay module
+
+## Thing Configuration
+
+### Bridge
+
+The Bridge thing has two parameters:
+
+- ipAddress: This is the IP address of the IP interface module
+- port: The listening port (optional, defaults to 2300)
+
+```
+Bridge luxom:bridge:myhouse [ ipAddress="192.168.0.50", port="2300"] {
+ ...
+}
+```
+
+### Devices
+
+Each device has an address on the Luxom bus, this address must be specified in the 'address' parameter.
+You will have to look it up in your documentation or in the 'Luxom Plusconfig' software.
+
+Sometimes a device does not send back a confirmation over the bus having set the correct state.
+Some dimmers do the dimming, but do not send back the set brightness level.
+To be able to use these devices, you can add the `doesNotReply=true` parameter so that the binding immediately sets the item's state and does not wait for confirmation.
+
+#### Dimmers
+
+Dimmers support the optional advanced parameters `onLevel`, `onToLast` and `stepPercentage`:
+
+* The `onLevel` parameter specifies the level to which the dimmer will go when sent an ON command. It defaults to 100.
+* The `onToLast` parameter is a boolean that defaults to false. If set to "true", the dimmer will go to its last non-zero level when sent an ON command. If the last non-zero level cannot be determined, the value of `onLevel` will be used instead.
+* The `stepPercentage` specifies the in-/decrease in percentage of brightness. Default is 5.
+
+A **dimmer** thing has a single channel *Lighting.Brightness* with type Dimmer and category DimmableLight.
+
+Thing configuration file example:
+
+```
+Thing dimmer dimmerLightLiving1 [address="A,02", onLevel="50", onToLast="false", stepPercentage="5"]
+```
+
+#### Switches
+
+Switches take no additional parameters.
+A **switch** thing has a single channel *switch* with type Switch and category Switch.
+
+Thing configuration file example:
+
+```
+Thing switch switchLiving1 [address="A,02"]
+```
+
+### Channels
+
+The following is a summary of channels for all Luxom things:
+
+| Thing | Channel | Item Type | Description |
+|---------------------|----------------|---------------|-----------------------------------|
+| dimmer | brightness | Dimmer | Increase/decrease the light level |
+| switch | switch | Switch | Switch the device on/off |
+
+
+### Full Example
+
+demo.things:
+
+```
+Bridge luxom:bridge:myhouse [ ipAddress="192.168.0.50", port="2300"] {
+ Thing switch switchBedroom1 "Switch 1" @ "Bedroom" [address="1,01"]
+ Thing dimmer dimmerBedroom1 "dimmer 1" @ "Bedroom" [address="A,02"]
+ Thing dimmer dimmerKitchen1 "dimmer 1" @ "Kitchen" [address="A,04", doesNotReply=true]
+}
+```
+
+demo.items:
+
+```
+Dimmer FF_Bedroom_Lights "Bedroom dimmer light" (FF_Living, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerBedroom1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"}
+Switch FF_Bedroom_PowerOutlet1 "Bedroom Power Outlet 1" (FF_Living, gPower) ["Switchable"] {channel="luxom:switch:myhouse:switchBedroom1:switch", ga="Outlet"}
+Dimmer FF_Kitchen_Lights "Kitchen dimmer light" (FF_Kitchen, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerKitchen1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"}
+```
diff --git a/bundles/org.openhab.binding.luxom/pom.xml b/bundles/org.openhab.binding.luxom/pom.xml
new file mode 100644
index 000000000..fe49117f5
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.luxom
+
+ openHAB Add-ons :: Bundles :: Luxom Binding
+
+
diff --git a/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml b/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml
new file mode 100644
index 000000000..df4159c59
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.luxom/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java
new file mode 100644
index 000000000..d974cd92b
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link LuxomBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomBindingConstants {
+
+ public static final String BINDING_ID = "luxom";
+
+ // List of all Thing Type UIDs
+
+ // bridge
+ public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge");
+
+ // generic thing types
+ public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
+ public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_THING_TYPE, THING_TYPE_SWITCH,
+ THING_TYPE_DIMMER);
+
+ // List of all Channel ids
+ public static final String CHANNEL_BRIGHTNESS = "brightness";
+ public static final String CHANNEL_SWITCH = "switch";
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java
new file mode 100644
index 000000000..57ea4e7f8
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.luxom.internal.handler.LuxomBridgeHandler;
+import org.openhab.binding.luxom.internal.handler.LuxomDimmerHandler;
+import org.openhab.binding.luxom.internal.handler.LuxomSwitchHandler;
+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;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link LuxomHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.luxom", service = ThingHandlerFactory.class)
+public class LuxomHandlerFactory extends BaseThingHandlerFactory {
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return LuxomBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ if (LuxomBindingConstants.BRIDGE_THING_TYPE.equals(thing.getThingTypeUID())) {
+ return new LuxomBridgeHandler((Bridge) thing);
+ } else if (LuxomBindingConstants.THING_TYPE_SWITCH.equals(thing.getThingTypeUID())) {
+ return new LuxomSwitchHandler(thing);
+ } else if (LuxomBindingConstants.THING_TYPE_DIMMER.equals(thing.getThingTypeUID())) {
+ return new LuxomDimmerHandler(thing);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java
new file mode 100644
index 000000000..ff16b48ee
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class CommandExecutionSpecification {
+ private final String command;
+
+ public CommandExecutionSpecification(String command) {
+ this.command = command;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java
new file mode 100644
index 000000000..c1e5e1cdb
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.luxom.internal.handler.config.LuxomBridgeConfig;
+import org.openhab.binding.luxom.internal.protocol.LuxomAction;
+import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
+import org.openhab.binding.luxom.internal.protocol.LuxomCommunication;
+import org.openhab.binding.luxom.internal.protocol.LuxomSystemInfo;
+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.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler responsible for communicating with the main Luxom IP access module.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomBridgeHandler extends BaseBridgeHandler {
+ public static final int HEARTBEAT_INTERVAL_SECONDS = 50;
+ private final LuxomSystemInfo systemInfo;
+
+ private static final int DEFAULT_RECONNECT_INTERVAL_IN_MINUTES = 1;
+ private static final long HEARTBEAT_ACK_TIMEOUT_SECONDS = 20;
+
+ private final Logger logger = LoggerFactory.getLogger(LuxomBridgeHandler.class);
+
+ private @Nullable LuxomBridgeConfig config;
+ private final AtomicInteger nrOfSendPermits = new AtomicInteger(0);
+ private int reconnectInterval;
+
+ private @Nullable LuxomCommand previousCommand;
+ private final LuxomCommunication communication;
+ private final BlockingQueue> sendQueue = new LinkedBlockingQueue<>();
+
+ private @Nullable Thread messageSender;
+ private @Nullable ScheduledFuture> heartBeat;
+ private @Nullable ScheduledFuture> heartBeatTimeoutTask;
+ private @Nullable ScheduledFuture> connectRetryJob;
+
+ public @Nullable LuxomBridgeConfig getIPBridgeConfig() {
+ return config;
+ }
+
+ public LuxomBridgeHandler(Bridge bridge) {
+ super(bridge);
+ logger.debug("Luxom bridge init");
+ systemInfo = new LuxomSystemInfo();
+ communication = new LuxomCommunication(this);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Bridge received command {} for {}", command.toFullString(), channelUID);
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfig().as(LuxomBridgeConfig.class);
+
+ if (validConfiguration(config)) {
+ reconnectInterval = (config.reconnectInterval > 0) ? config.reconnectInterval
+ : DEFAULT_RECONNECT_INTERVAL_IN_MINUTES;
+
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.connecting");
+ scheduler.submit(this::connect); // start the async connect task
+ }
+ }
+
+ private boolean validConfiguration(@Nullable LuxomBridgeConfig config) {
+ if (config == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/bridge-configuration-missing");
+
+ return false;
+ }
+
+ if (config.ipAddress == null || config.ipAddress.trim().isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/bridge-address-missing");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private void scheduleConnectRetry(long waitMinutes) {
+ logger.debug("Scheduling connection retry in {} (minutes)", waitMinutes);
+ connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
+ }
+
+ private synchronized void connect() {
+ if (communication.isConnected()) {
+ return;
+ }
+
+ if (config != null) {
+ logger.debug("Connecting to bridge at {}", config.ipAddress);
+ }
+
+ try {
+ communication.startCommunication();
+ } catch (Exception e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ disconnect();
+ scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
+ }
+ }
+
+ public void startProcessing() {
+ nrOfSendPermits.set(1);
+
+ updateStatus(ThingStatus.ONLINE);
+
+ messageSender = new Thread(this::sendCommandsThread, "Luxom sender");
+ messageSender.start();
+
+ logger.debug("Starting heartbeat job with interval {} (seconds)", HEARTBEAT_INTERVAL_SECONDS);
+ heartBeat = scheduler.scheduleWithFixedDelay(this::sendHeartBeat, 10, HEARTBEAT_INTERVAL_SECONDS,
+ TimeUnit.SECONDS);
+ }
+
+ private void sendCommandsThread() {
+ logger.debug("Starting send commands thread...");
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ logger.debug("waiting for command to send...");
+ List commands = sendQueue.take();
+
+ try {
+ for (CommandExecutionSpecification commandExecutionSpecification : commands) {
+ communication.sendMessage(commandExecutionSpecification.getCommand());
+ }
+ } catch (IOException e) {
+ logger.warn("Communication error while sending, will try to reconnect. Error: {}", e.getMessage());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+
+ reconnect();
+
+ // reconnect() will start a new thread; terminate this one
+ break;
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private synchronized void disconnect() {
+ logger.debug("Disconnecting from bridge");
+
+ if (connectRetryJob != null) {
+ connectRetryJob.cancel(true);
+ }
+
+ if (this.heartBeat != null) {
+ this.heartBeat.cancel(true);
+ }
+
+ cancelCheckAliveTimeoutTask();
+
+ if (messageSender != null && messageSender.isAlive()) {
+ messageSender.interrupt();
+ }
+
+ this.communication.stopCommunication();
+ }
+
+ public void reconnect() {
+ reconnect(false);
+ }
+
+ private synchronized void reconnect(boolean timeout) {
+ if (timeout) {
+ logger.debug("Keepalive timeout, attempting to reconnect to the bridge");
+ } else {
+ logger.debug("Connection problem, attempting to reconnect to the bridge");
+ }
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ disconnect();
+ connect();
+ }
+
+ public void sendCommands(List commands) {
+ this.sendQueue.add(commands);
+ }
+
+ private @Nullable LuxomThingHandler findThingHandler(@Nullable String address) {
+ for (Thing thing : getThing().getThings()) {
+ if (thing.getHandler() instanceof LuxomThingHandler) {
+ LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
+
+ try {
+ if (handler != null && handler.getAddress().equals(address)) {
+ return handler;
+ }
+ } catch (IllegalStateException e) {
+ logger.trace("Handler for id {} not initialized", address);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * needed with fast reconnect to update status of things
+ */
+ public void forceRefreshThings() {
+ for (Thing thing : getThing().getThings()) {
+ if (thing.getHandler() instanceof LuxomThingHandler) {
+ LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
+ handler.ping();
+ }
+ }
+ }
+
+ private void sendHeartBeat() {
+ logger.trace("Sending heartbeat");
+ // Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
+ heartBeatTimeoutTask = scheduler.schedule(() -> reconnect(true), HEARTBEAT_ACK_TIMEOUT_SECONDS,
+ TimeUnit.SECONDS);
+ sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.HEARTBEAT.getCommand())));
+ }
+
+ @Override
+ public void thingUpdated(Thing thing) {
+ LuxomBridgeConfig newConfig = thing.getConfiguration().as(LuxomBridgeConfig.class);
+ boolean validConfig = validConfiguration(newConfig);
+ boolean needsReconnect = validConfig && config != null && !config.sameConnectionParameters(newConfig);
+
+ if (!validConfig || needsReconnect) {
+ dispose();
+ }
+
+ this.thing = thing;
+ this.config = newConfig;
+
+ if (needsReconnect) {
+ initialize();
+ }
+ }
+
+ public void handleCommunicationError(IOException e) {
+ logger.debug("Communication error while reading, will try to reconnect. Error: {}", e.getMessage());
+ reconnect();
+ }
+
+ @Override
+ public void dispose() {
+ disconnect();
+ }
+
+ public void handleIncomingLuxomMessage(String luxomMessage) throws IOException {
+ cancelCheckAliveTimeoutTask(); // we got a message
+
+ logger.trace("Luxom: received {}", luxomMessage);
+ LuxomCommand luxomCommand = new LuxomCommand(luxomMessage);
+
+ // Now dispatch update to the proper thing handler
+
+ if (LuxomAction.PASSWORD_REQUEST == luxomCommand.getAction()) {
+ communication.sendMessage(LuxomAction.REQUEST_FOR_INFORMATION.getCommand()); // direct send, no queue, so
+ // no tcp flow constraint
+ } else if (LuxomAction.MODULE_INFORMATION == luxomCommand.getAction()) {
+ cmdSystemInfo(luxomCommand.getData());
+ if (ThingStatus.ONLINE != getThing().getStatus()) {
+ // this all happens before TCP flow controle, when startProcessing is called, TCP flow is activated...
+ startProcessing();
+ }
+ } else if (LuxomAction.ACKNOWLEDGE == luxomCommand.getAction()) {
+ logger.trace("received acknowledgement");
+ } else if (LuxomAction.DATA == luxomCommand.getAction()
+ || LuxomAction.DATA_RESPONSE == luxomCommand.getAction()) {
+ previousCommand = luxomCommand;
+ } else if (LuxomAction.INVALID_ACTION != luxomCommand.getAction()) {
+ if (LuxomAction.DATA_BYTE == luxomCommand.getAction()
+ || LuxomAction.DATA_BYTE_RESPONSE == luxomCommand.getAction()) {
+ // data for previous command if it needs it
+ if (previousCommand != null && previousCommand.getAction().isNeedsData()) {
+ previousCommand.setData(luxomCommand.getData());
+ luxomCommand = previousCommand;
+ previousCommand = null;
+ }
+ }
+
+ if (luxomCommand != null) {
+ LuxomThingHandler handler = findThingHandler(luxomCommand.getAddress());
+
+ if (handler != null) {
+ handler.handleCommandComingFromBridge(luxomCommand);
+ } else {
+ logger.warn("No handler found command {} for address : {}", luxomMessage,
+ luxomCommand.getAddress());
+ }
+ } else {
+ logger.warn("Something was wrong with the order of incoming commands, resulting command is null");
+ }
+ } else {
+ logger.trace("Luxom: not handled {}", luxomMessage);
+ }
+ logger.trace("nrOfPermits after receive: {}", nrOfSendPermits.get());
+ }
+
+ private void cancelCheckAliveTimeoutTask() {
+ var task = heartBeatTimeoutTask;
+ if (task != null) {
+ // This method can be called from the keepAliveReconnect thread. Make sure
+ // we don't interrupt ourselves, as that may prevent the reconnection attempt.
+ task.cancel(false);
+ }
+ }
+
+ private synchronized void cmdSystemInfo(@Nullable String info) {
+ systemInfo.setSwVersion(info);
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java
new file mode 100644
index 000000000..1dc011a7b
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * exception during communication with luxom IP module
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomConnectionException extends Exception {
+ private static final long serialVersionUID = 654654L;
+
+ public LuxomConnectionException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java
new file mode 100644
index 000000000..e8d6db34b
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.luxom.internal.LuxomBindingConstants;
+import org.openhab.binding.luxom.internal.handler.config.LuxomThingDimmerConfig;
+import org.openhab.binding.luxom.internal.handler.util.PercentageConverter;
+import org.openhab.binding.luxom.internal.protocol.LuxomAction;
+import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+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.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LuxomDimmerHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomDimmerHandler extends LuxomThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(LuxomDimmerHandler.class);
+
+ public LuxomDimmerHandler(Thing thing) {
+ super(thing);
+ }
+
+ private @Nullable LuxomThingDimmerConfig config;
+ private final AtomicReference lastLightLevel = new AtomicReference<>(0);
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ config = getConfig().as(LuxomThingDimmerConfig.class);
+
+ logger.debug("Initializing Switch handler for address {}", getAddress());
+
+ initDeviceState();
+ }
+
+ @Override
+ protected void initDeviceState() {
+ logger.debug("Initializing device state for Switch {}", getAddress());
+ @Nullable
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ } else if (ThingStatus.ONLINE.equals(bridge.getStatus())) {
+ if (config != null && config.doesNotReply) {
+ logger.debug("Switch {} will not reply, so always keeping it ONLINE", getAddress());
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-initial-response");
+ ping(); // handleUpdate() will set thing status to online when response arrives
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("dimmer at address {} received command {} for {}", getAddress(), command.toFullString(),
+ channelUID);
+ if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) {
+ if (OnOffType.ON.equals(command)) {
+ set();
+ } else if (OnOffType.OFF.equals(command)) {
+ clear();
+ }
+ } else if (LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId()) && config != null) {
+ if (command instanceof Number) {
+ int level = ((Number) command).intValue();
+ logger.trace("dimmer at address {} just setting dimmer level", getAddress());
+ dim(level);
+ } else if (command instanceof IncreaseDecreaseType) {
+ IncreaseDecreaseType s = (IncreaseDecreaseType) command;
+ int currentValue = lastLightLevel.get();
+ int newValue;
+ if (IncreaseDecreaseType.INCREASE.equals(s)) {
+ newValue = currentValue + config.stepPercentage;
+ // round down to step multiple
+ newValue = newValue - newValue % config.stepPercentage;
+ logger.trace("dimmer at address {} just increasing dimmer level", getAddress());
+ dim(newValue);
+ } else {
+ newValue = currentValue - config.stepPercentage;
+ // round up to step multiple
+ newValue = newValue + newValue % config.stepPercentage;
+ logger.trace("dimmer at address {} just increasing dimmer level", getAddress());
+ dim(Math.max(newValue, 0));
+ }
+ } else if (OnOffType.ON.equals(command)) {
+ if (config.onToLast) {
+ dim(lastLightLevel.get());
+ } else {
+ dim(config.onLevel.intValue());
+ }
+ } else if (OnOffType.OFF.equals(command)) {
+ dim(0);
+ }
+ }
+ }
+
+ @Override
+ public void handleCommandComingFromBridge(LuxomCommand command) {
+ updateStatus(ThingStatus.ONLINE);
+ if (LuxomAction.CLEAR_RESPONSE.equals(command.getAction())) {
+ updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.OFF);
+ } else if (LuxomAction.SET_RESPONSE.equals(command.getAction())) {
+ updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.ON);
+ } else if (LuxomAction.DATA_RESPONSE.equals(command.getAction())) {
+ int percentage = PercentageConverter.getPercentage(command.getData());
+
+ lastLightLevel.set(percentage);
+ updateState(LuxomBindingConstants.CHANNEL_BRIGHTNESS, new PercentType(percentage));
+ }
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ logger.debug("dimmer at address {} linked to channel {}", getAddress(), channelUID);
+ if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())
+ || LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
+ // Refresh state when new item is linked.
+ if (config != null && !config.doesNotReply) {
+ ping();
+ }
+ }
+ }
+
+ /**
+ * example : *A,0,2,2B;*Z,057;
+ */
+ private void dim(int percentage) {
+ logger.debug("dimming dimmer at address {} to {} %", getAddress(), percentage);
+ List commands = new ArrayList<>(3);
+ if (percentage == 0) {
+ commands.add(new CommandExecutionSpecification(LuxomAction.CLEAR.getCommand() + ",0," + getAddress()));
+ } else {
+ commands.add(new CommandExecutionSpecification(LuxomAction.SET.getCommand() + ",0," + getAddress()));
+ }
+ commands.add(new CommandExecutionSpecification(LuxomAction.DATA.getCommand() + ",0," + getAddress()));
+ commands.add(new CommandExecutionSpecification(
+ LuxomAction.DATA_BYTE.getCommand() + ",0" + PercentageConverter.getHexRepresentation(percentage)));
+
+ sendCommands(commands);
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java
new file mode 100644
index 000000000..3b27fb12e
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.luxom.internal.LuxomBindingConstants;
+import org.openhab.binding.luxom.internal.protocol.LuxomAction;
+import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
+import org.openhab.core.library.types.OnOffType;
+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.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LuxomSwitchHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomSwitchHandler extends LuxomThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(LuxomSwitchHandler.class);
+
+ public LuxomSwitchHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+
+ logger.debug("Initializing Switch handler for address {}", getAddress());
+
+ initDeviceState();
+ }
+
+ @Override
+ protected void initDeviceState() {
+ logger.debug("Initializing device state for Switch {}", getAddress());
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ } else if (ThingStatus.ONLINE.equals(bridge.getStatus())) {
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-initial-response");
+ ping(); // handleUpdate() will set thing status to online when response arrives
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("switch at address {} received command {} for {}", getAddress(), command.toFullString(),
+ channelUID);
+ if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) {
+ if (OnOffType.ON.equals(command)) {
+ set();
+ ping(); // to make sure we know the current state
+ } else if (OnOffType.OFF.equals(command)) {
+ clear();
+ ping(); // to make sure we know the current state
+ }
+ }
+ }
+
+ @Override
+ public void handleCommandComingFromBridge(LuxomCommand command) {
+ if (LuxomAction.CLEAR_RESPONSE.equals(command.getAction())) {
+ updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.OFF);
+ updateStatus(ThingStatus.ONLINE);
+ } else if (LuxomAction.SET_RESPONSE.equals(command.getAction())) {
+ updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.ON);
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ logger.debug("switch at address {} linked to channel {}", getAddress(), channelUID);
+ if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())
+ || LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
+ // Refresh state when new item is linked.
+ ping();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java
new file mode 100644
index 000000000..cf8285023
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.luxom.internal.protocol.LuxomAction;
+import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base type for all Luxom thing handlers.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public abstract class LuxomThingHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(LuxomThingHandler.class);
+
+ private String address = "";
+
+ @Override
+ public void initialize() {
+ String id = (String) getConfig().get("address");
+ if (id == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/status.thing-address-missing");
+ address = "noaddress";
+ return;
+ }
+ address = id;
+ }
+
+ public LuxomThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ public abstract void handleCommandComingFromBridge(LuxomCommand command);
+
+ public final String getAddress() {
+ return address;
+ }
+
+ /**
+ * Queries for any device state needed at initialization time or after losing connectivity to the bridge, and
+ * updates device status. Will be called when bridge status changes to ONLINE and thing has status
+ * OFFLINE:BRIDGE_OFFLINE.
+ */
+ protected abstract void initDeviceState();
+
+ /**
+ * Called when changing thing status to offline. Subclasses may override to take any needed actions.
+ */
+ protected void thingOfflineNotify() {
+ }
+
+ protected @Nullable LuxomBridgeHandler getBridgeHandler() {
+ Bridge bridge = getBridge();
+
+ return bridge == null ? null : (LuxomBridgeHandler) bridge.getHandler();
+ }
+
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ logger.debug("Bridge status changed to {} for luxom device handler {}", bridgeStatusInfo.getStatus(),
+ getAddress());
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
+ && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
+ initDeviceState();
+
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ thingOfflineNotify();
+ }
+ }
+
+ protected void sendCommands(List commands) {
+ @Nullable
+ LuxomBridgeHandler bridgeHandler = getBridgeHandler();
+
+ if (bridgeHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR,
+ "@text/status.bridge-handler-missing");
+ thingOfflineNotify();
+ } else {
+ bridgeHandler.sendCommands(commands);
+ }
+ }
+
+ /**
+ * example : *P,0,1,21;
+ */
+ protected void ping() {
+ sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.PING.getCommand() + ",0," + getAddress())));
+ }
+
+ /**
+ * example : *S,0,1,21;
+ */
+ protected void set() {
+ sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.SET.getCommand() + ",0," + getAddress())));
+ }
+
+ /**
+ * example : *C,0,1,21;
+ */
+ protected void clear() {
+ sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.CLEAR.getCommand() + ",0," + getAddress())));
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java
new file mode 100644
index 000000000..52bbd5c83
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler.config;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link LuxomBridgeConfig} is the general config class for Luxom Bridges.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomBridgeConfig {
+ public @Nullable String ipAddress;
+ public int port;
+
+ /**
+ * reconnect after X minutes when disconnected
+ */
+ public int reconnectInterval;
+ public int aliveCheckInterval;
+
+ /**
+ * if true, on communication error the devices will NOT go offline...
+ * if false, they will go offline. In both instances they will get (re)pinged after reconnect.
+ *
+ */
+ public boolean useFastReconnect = false;
+
+ public boolean sameConnectionParameters(LuxomBridgeConfig config) {
+ return Objects.equals(ipAddress, config.ipAddress) && config.port == port
+ && (reconnectInterval == config.reconnectInterval) && (aliveCheckInterval == config.aliveCheckInterval);
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java
new file mode 100644
index 000000000..7a7de1a1a
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link LuxomThingConfig} is the general config class for luxom things.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomThingConfig {
+ public Boolean doesNotReply = Boolean.FALSE;
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java
new file mode 100644
index 000000000..84c25ea17
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler.config;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link LuxomThingDimmerConfig} is the config class for Niko Home Control Dimmer Actions.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomThingDimmerConfig extends LuxomThingConfig {
+ private static final int DEFAULT_ONLEVEL = 100;
+
+ public BigDecimal onLevel = new BigDecimal(DEFAULT_ONLEVEL);
+ public Boolean onToLast = Boolean.FALSE;
+ public Integer stepPercentage = 5;
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java
new file mode 100644
index 000000000..965943ee3
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.handler.util;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * converts the hexadecimal string representation to a integer value between 0 - 100
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class PercentageConverter {
+ /**
+ * @param hexRepresentation
+ * @return if hexRepresentation == null return -1, otherwise return percentage
+ */
+ public static int getPercentage(@Nullable String hexRepresentation) {
+ if (hexRepresentation == null)
+ return -1;
+ int decimal = Integer.parseInt(hexRepresentation, 16);
+ BigDecimal level = new BigDecimal(100 * decimal).divide(new BigDecimal(255), RoundingMode.FLOOR);
+ return level.intValue();
+ }
+
+ public static String getHexRepresentation(int percentage) {
+ BigDecimal decimal = new BigDecimal(255 * percentage).divide(new BigDecimal(100), RoundingMode.CEILING);
+ return Integer.toHexString(decimal.intValue()).toUpperCase();
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java
new file mode 100644
index 000000000..bf7fba4a8
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * luxom action
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public enum LuxomAction {
+ HEARTBEAT("*U", false),
+ ACKNOWLEDGE("@1*V", false),
+ TOGGLE("*T", true),
+ PING("*P", true),
+ MODULE_INFORMATION("*!", false),
+ PASSWORD_REQUEST("@1*PW-", false),
+ CLEAR_RESPONSE("@1*C", true),
+ SET_RESPONSE("@1*S", true),
+ DATA_RESPONSE("@1*A", true, true),
+ DATA_BYTE_RESPONSE("@1*Z", false),
+ DATA("*A", true, true),
+ DATA_BYTE("*Z", false),
+ SET("*S", true),
+ CLEAR("*C", true),
+ REQUEST_FOR_INFORMATION("*?", false),
+ INVALID_ACTION("-INVALID-", false); // this is not part of the luxom api, it's for internal use.;
+
+ private final String command;
+ private final boolean hasAddress;
+ private final boolean needsData;
+
+ LuxomAction(String command, boolean hasAddress) {
+ this(command, hasAddress, false);
+ }
+
+ LuxomAction(String command, boolean hasAddress, boolean needsData) {
+ this.command = command;
+ this.hasAddress = hasAddress;
+ this.needsData = needsData;
+ }
+
+ public static LuxomAction of(String command) {
+ return Arrays.stream(LuxomAction.values()).filter(a -> a.getCommand().equals(command)).findFirst()
+ .orElse(INVALID_ACTION);
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public boolean isHasAddress() {
+ return hasAddress;
+ }
+
+ public boolean isNeedsData() {
+ return needsData;
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java
new file mode 100644
index 000000000..ce3980244
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * luxom command
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomCommand {
+ private final LuxomAction action;
+ private final @Nullable String address; // must for data byte commands be set after construction
+
+ private @Nullable String data;
+
+ public LuxomCommand(String command) {
+ if (command.length() == 0) {
+ action = LuxomAction.INVALID_ACTION;
+ data = command;
+ address = null;
+ return;
+ }
+ String[] parts = command.split(",");
+
+ if (parts.length == 1) {
+ if (command.startsWith(LuxomAction.MODULE_INFORMATION.getCommand())) {
+ action = LuxomAction.MODULE_INFORMATION;
+ data = command.substring(2);
+ } else if (command.equals(LuxomAction.PASSWORD_REQUEST.getCommand())) {
+ action = LuxomAction.PASSWORD_REQUEST;
+ data = null;
+ } else if (command.equals(LuxomAction.ACKNOWLEDGE.getCommand())) {
+ action = LuxomAction.ACKNOWLEDGE;
+ data = null;
+ } else {
+ action = LuxomAction.INVALID_ACTION;
+ data = command;
+ }
+ address = null;
+ } else {
+ action = LuxomAction.of(parts[0]);
+ StringBuilder stringBuilder = new StringBuilder();
+ if (action.isHasAddress()) {
+ // first 0 not needed ?
+ for (int i = 2; i < parts.length; i++) {
+ stringBuilder.append(parts[i]);
+ if (i != (parts.length - 1)) {
+ stringBuilder.append(",");
+ }
+ }
+ address = stringBuilder.toString();
+ data = null;
+ } else {
+ for (int i = 1; i < parts.length; i++) {
+ stringBuilder.append(parts[i]);
+ if (i != (parts.length - 1)) {
+ stringBuilder.append(",");
+ }
+ }
+ address = null;
+ data = stringBuilder.toString();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "LuxomCommand{" + "action=" + action + ", address='" + address + '\'' + ", data='" + data + '\'' + '}';
+ }
+
+ public LuxomAction getAction() {
+ return action;
+ }
+
+ @Nullable
+ public String getData() {
+ return data;
+ }
+
+ @Nullable
+ public String getAddress() {
+ return address;
+ }
+
+ public void setData(@Nullable String data) {
+ this.data = data;
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java
new file mode 100644
index 000000000..9dc0a97a2
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java
@@ -0,0 +1,210 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.luxom.internal.handler.LuxomBridgeHandler;
+import org.openhab.binding.luxom.internal.handler.LuxomConnectionException;
+import org.openhab.binding.luxom.internal.handler.config.LuxomBridgeConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LuxomCommunication} class is able to do the following tasks with Luxom IP
+ * systems:
+ *
+ *
Start and stop TCP socket connection with Luxom IP-interface.
+ *
Read all setup and status information from the Luxom Controller.
+ *
Execute Luxom commands.
+ *
Listen to events from Luxom.
+ *
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public class LuxomCommunication {
+
+ private final Logger logger = LoggerFactory.getLogger(LuxomCommunication.class);
+
+ private final LuxomBridgeHandler bridgeHandler;
+
+ private @Nullable Socket luxomSocket;
+ private @Nullable PrintWriter luxomOut;
+ private @Nullable BufferedReader luxomIn;
+
+ private volatile boolean listenerStopped;
+ private volatile boolean stillListeningToEvents;
+
+ public LuxomCommunication(LuxomBridgeHandler luxomBridgeHandler) {
+ super();
+ bridgeHandler = luxomBridgeHandler;
+ }
+
+ public synchronized void startCommunication() throws LuxomConnectionException {
+ try {
+ waitForEventListenerThreadToStop();
+
+ initializeSocket();
+
+ // Start Luxom event listener. This listener will act on all messages coming from
+ // IP-interface.
+ (new Thread(this::runLuxomEvents,
+ "OH-binding-" + bridgeHandler.getThing().getBridgeUID() + "-listen-for-events")).start();
+
+ } catch (IOException | InterruptedException e) {
+ throw new LuxomConnectionException(e);
+ }
+ }
+
+ private void waitForEventListenerThreadToStop() throws InterruptedException, IOException {
+ for (int i = 1; stillListeningToEvents && (i <= 5); i++) {
+ // the events listener thread did not finish yet, so wait max 5000ms before restarting
+ // noinspection BusyWait
+ Thread.sleep(1000);
+ }
+ if (stillListeningToEvents) {
+ throw new IOException("starting but previous connection still active after 5000ms");
+ }
+ }
+
+ private void initializeSocket() throws IOException {
+ LuxomBridgeConfig luxomBridgeConfig = bridgeHandler.getIPBridgeConfig();
+ if (luxomBridgeConfig != null) {
+ InetAddress addr = InetAddress.getByName(luxomBridgeConfig.ipAddress);
+ int port = luxomBridgeConfig.port;
+
+ luxomSocket = new Socket(addr, port);
+ luxomSocket.setReuseAddress(true);
+ luxomSocket.setKeepAlive(true);
+ luxomOut = new PrintWriter(luxomSocket.getOutputStream());
+ luxomIn = new BufferedReader(new InputStreamReader(luxomSocket.getInputStream()));
+ logger.debug("Luxom: connected via local port {}", luxomSocket.getLocalPort());
+ } else {
+ logger.warn("Luxom: ip bridge not initialized");
+ }
+ }
+
+ /**
+ * Cleanup socket when the communication with Luxom IP-interface is closed.
+ */
+ public synchronized void stopCommunication() {
+ listenerStopped = true;
+
+ closeSocket();
+ }
+
+ private void closeSocket() {
+ if (luxomSocket != null) {
+ try {
+ luxomSocket.close();
+ } catch (IOException ignore) {
+ // ignore IO Error when trying to close the socket if the intention is to close it anyway
+ }
+ }
+ luxomSocket = null;
+
+ logger.debug("Luxom: communication stopped");
+ }
+
+ /**
+ * Method that handles inbound communication from Luxom, to be called on a separate thread.
+ *
+ * The thread listens to the TCP socket opened at instantiation of the {@link LuxomCommunication} class
+ * and interprets all inbound json messages. It triggers state updates for active channels linked to the Niko Home
+ * Control actions. It is started after initialization of the communication.
+ */
+ private void runLuxomEvents() {
+ StringBuilder luxomMessage = new StringBuilder();
+
+ logger.debug("Luxom: listening for events");
+ listenerStopped = false;
+ stillListeningToEvents = true;
+
+ try {
+ boolean mayUseFastReconnect = false;
+ boolean mustDoFullReconnect = false;
+ while (!listenerStopped && (luxomIn != null)) {
+ int nextChar = luxomIn.read();
+ if (nextChar == -1) {
+ logger.trace("Luxom: stream ends unexpectedly...");
+ LuxomBridgeConfig luxomBridgeConfig = bridgeHandler.getIPBridgeConfig();
+ if (mayUseFastReconnect && luxomBridgeConfig != null && luxomBridgeConfig.useFastReconnect) {
+ // we stay in the loop and just reinitialize socket
+ mayUseFastReconnect = false; // just once use fast reconnect
+ this.closeSocket();
+ this.initializeSocket();
+ // followed by forced update of status
+ bridgeHandler.forceRefreshThings();
+ } else {
+ listenerStopped = true;
+ mustDoFullReconnect = true;
+ }
+ } else {
+ mayUseFastReconnect = true; // reset
+ char c = (char) nextChar;
+ logger.trace("Luxom: read char {}", c);
+
+ luxomMessage.append(c);
+
+ if (';' == c) {
+ String message = luxomMessage.toString();
+ bridgeHandler.handleIncomingLuxomMessage(message.substring(0, message.length() - 1));
+ luxomMessage = new StringBuilder();
+ }
+ }
+ }
+ if (mustDoFullReconnect) {
+ // I want to do this out of the loop
+ bridgeHandler.reconnect();
+ }
+ logger.trace("Luxom: stopped listening to events");
+ } catch (IOException e) {
+ logger.warn("Luxom: listening to events - IO exception", e);
+ if (!listenerStopped) {
+ stillListeningToEvents = false;
+ // this is a socket error, not a communication stop triggered from outside this runnable
+ // the IO has stopped working, so we need to close cleanly and try to restart
+ bridgeHandler.handleCommunicationError(e);
+ return;
+ }
+ } finally {
+ stillListeningToEvents = false;
+ }
+
+ // this is a stop from outside the runnable, so just log it and stop
+ logger.debug("Luxom: event listener thread stopped");
+ }
+
+ public synchronized void sendMessage(String message) throws IOException {
+ logger.debug("Luxom: send {}", message);
+ if (luxomOut != null) {
+ luxomOut.print(message + ";");
+ luxomOut.flush();
+ if (luxomOut.checkError()) {
+ throw new IOException(String.format("luxom communication error when sending message: %s", message));
+ }
+ }
+ }
+
+ public boolean isConnected() {
+ return luxomSocket != null && luxomSocket.isConnected();
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java
new file mode 100644
index 000000000..7e20eabf9
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link LuxomSystemInfo} class represents the systeminfo Luxom communication object. It contains all
+ * Luxom system data received from the Luxom IP controller when initializing the connection.
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+public final class LuxomSystemInfo {
+
+ private @Nullable String swVersion = "";
+
+ public @Nullable String getSwVersion() {
+ return swVersion;
+ }
+
+ public void setSwVersion(@Nullable String swVersion) {
+ this.swVersion = swVersion;
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..8f0c1d886
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,8 @@
+
+
+
+ Luxom Binding
+ This is the binding for Luxom bus system (https://www.luxom.io/)
+
diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties
new file mode 100644
index 000000000..0b7aa475c
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties
@@ -0,0 +1,38 @@
+# binding
+
+binding.luxom.name = Luxom Binding
+binding.luxom.description = This is the binding for Luxom bus system (https://www.luxom.io/)
+
+# thing types
+
+thing-type.luxom.switch.label = Switch
+thing-type.luxom.switch.description = Switch type action in Luxom
+thing-type.luxom.dimmer.label = Dimmer
+thing-type.luxom.dimmer.description = Dimmer type action in Luxom
+
+# thing types config
+
+thing-type.config.luxom.switch.address.label = Address
+thing-type.config.luxom.switch.address.description = Luxom bus address
+thing-type.config.luxom.dimmer.address.label = Address
+thing-type.config.luxom.dimmer.address.description = Luxom bus address
+thing-type.config.luxom.dimmer.onLevel.label = On Level
+thing-type.config.luxom.dimmer.onLevel.description = Output level to go to when an ON command is received. Default is 100%.
+thing-type.config.luxom.dimmer.onToLast.label = Turn On To Last Level
+thing-type.config.luxom.dimmer.onToLast.description = If set to true, dimmer will go to the last non-zero level set when an ON command is received. If the last level cannot be determined, the value of onLevel will be used instead.
+thing-type.config.luxom.dimmer.stepPercentage.label = Step Value
+thing-type.config.luxom.dimmer.stepPercentage.description = Step value used for increase/decrease of dimmer brightness, default 5%
+
+# channel types
+
+channel-type.luxom.button.label = Button
+channel-type.luxom.button.description = Pushbutton control for action in Luxom
+
+# messages
+status.awaiting-initial-response = Awaiting initial response
+status.bridge-configuration-missing = Bridge configuration missing
+status.connecting = Connecting
+status.bridge-address-missing = Bridge address not specified
+status.bridge-initializing = Luxom bridge is initializing
+status.bridge-handler-missing = No bridge associated
+status.thing-address-missing = Address is missing
diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..58060d25e
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+ This bridge represents a Luxom IP-interface (for example a DS-65L)
+
+
+ network-address
+
+ The IP or host name of the Luxom IP-interface
+
+
+
+ Port to communicate with Luxom IP-interface, default 2300
+ 2300
+ true
+
+
+
+ The period in minutes that the handler will wait between connection attempts after disconnect
+ minutes
+ 1
+ true
+
+
+
+
+
+
+
+
+
+ Luxom Switch
+
+
+
+ address
+
+
+
+ Luxom bus address
+ false
+
+
+
+
+
+
+
+
+ Luxom Dimmer
+
+
+
+
+
+
+ Luxom bus address
+ false
+
+
+
+ Output level to go to when an ON command is received. Default is 100%.
+ 100
+ true
+
+
+
+
+ If set to true, dimmer will go to the last non-zero level set when an ON command is received. If the
+ last level cannot be determined, the value of onLevel will be used instead.
+
+ false
+ true
+
+
+
+ Step value used for increase/decrease of dimmer brightness, default 5%
+ 5
+ true
+
+
+
+
+
+ Switch
+
+ Switch control for action in Luxom
+ Switch
+ veto
+
+
diff --git a/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java
new file mode 100644
index 000000000..ddeb79527
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+class LuxomCommandTest {
+
+ @Test
+ void parsePulseCommand() {
+ LuxomCommand command = new LuxomCommand("*P,0,1,04");
+
+ assertEquals(LuxomAction.PING, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parsePasswordRequest() {
+ LuxomCommand command = new LuxomCommand(LuxomAction.PASSWORD_REQUEST.getCommand());
+
+ assertEquals(LuxomAction.PASSWORD_REQUEST, command.getAction());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseClearCommand() {
+ LuxomCommand command = new LuxomCommand("*C,0,1,04");
+
+ assertEquals(LuxomAction.CLEAR, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseClearResponse() {
+ LuxomCommand command = new LuxomCommand("@1*C,0,1,04");
+
+ assertEquals(LuxomAction.CLEAR_RESPONSE, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseClearResponse2() {
+ LuxomCommand command = new LuxomCommand("@1*C,0,1,04");
+
+ assertEquals(LuxomAction.CLEAR_RESPONSE, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseSetCommand() {
+ LuxomCommand command = new LuxomCommand("*S,0,1,04");
+
+ assertEquals(LuxomAction.SET, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseSetResponse() {
+ LuxomCommand command = new LuxomCommand("@1*S,0,1,04");
+
+ assertEquals(LuxomAction.SET_RESPONSE, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseDimCommand() {
+ LuxomCommand command = new LuxomCommand("*A,0,1,04");
+
+ assertEquals(LuxomAction.DATA, command.getAction());
+ assertEquals("1,04", command.getAddress());
+ assertNull(command.getData());
+ }
+
+ @Test
+ void parseDataCommand() {
+ LuxomCommand command = new LuxomCommand("*Z,048");
+
+ assertEquals(LuxomAction.DATA_BYTE, command.getAction());
+ assertEquals("048", command.getData());
+ assertNull(command.getAddress());
+ }
+
+ @Test
+ void parseDataResponseCommand() {
+ LuxomCommand command = new LuxomCommand("@1*Z,048");
+
+ assertEquals(LuxomAction.DATA_BYTE_RESPONSE, command.getAction());
+ assertEquals("048", command.getData());
+ assertNull(command.getAddress());
+ }
+}
diff --git a/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java
new file mode 100644
index 000000000..e40482062
--- /dev/null
+++ b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 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.luxom.internal.protocol;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.luxom.internal.handler.util.PercentageConverter;
+
+/**
+ *
+ * @author Kris Jespers - Initial contribution
+ */
+@NonNullByDefault
+class PercentageConverterTest {
+ @Test
+ void hexToPercentage() {
+ assertEquals(34, PercentageConverter.getPercentage("057"));
+ }
+
+ @Test
+ void hexToPercentage100() {
+ assertEquals(100, PercentageConverter.getPercentage("0FF"));
+ }
+
+ @Test
+ void percentageToHex() {
+ assertEquals("57", PercentageConverter.getHexRepresentation(34));
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 4e1d0c9a8..f2eb2e1bf 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -201,6 +201,7 @@
org.openhab.binding.loxoneorg.openhab.binding.luftdateninfoorg.openhab.binding.lutron
+ org.openhab.binding.luxomorg.openhab.binding.luxtronikheatpumporg.openhab.binding.magentatvorg.openhab.binding.mail