]> git.basschouten.com Git - openhab-addons.git/commitdiff
[luxom] Initial contribution (#12310)
authorjesperskriasoft <80278903+jesperskriasoft@users.noreply.github.com>
Tue, 5 Apr 2022 18:02:27 +0000 (20:02 +0200)
committerGitHub <noreply@github.com>
Tue, 5 Apr 2022 18:02:27 +0000 (20:02 +0200)
Signed-off-by: Kris Jespers <kriasoft@telenet.be>
28 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.luxom/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.luxom/README.md [new file with mode: 0644]
bundles/org.openhab.binding.luxom/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java [new file with mode: 0644]
bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java [new file with mode: 0644]
bundles/pom.xml

index b75c95be694b682e98ddb34012573c80a5237145..51771a55b964ac8e1c897da4bd16825c452b0cca 100644 (file)
 /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
index 9bd4c43c6c7b1563d199ef2a362cff2c779efa94..2a983e6dcb678c86cf994129b40b79e99aedc8b3 100644 (file)
       <artifactId>org.openhab.binding.lutron</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.luxom</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.luxtronikheatpump</artifactId>
diff --git a/bundles/org.openhab.binding.luxom/NOTICE b/bundles/org.openhab.binding.luxom/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..56a1a1d
--- /dev/null
@@ -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"   <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"   <poweroutlet>    (FF_Living, gPower)      ["Switchable"] {channel="luxom:switch:myhouse:switchBedroom1:switch", ga="Outlet"}
+Dimmer          FF_Kitchen_Lights             "Kitchen dimmer light"   <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 (file)
index 0000000..fe49117
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.luxom</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Luxom Binding</name>
+
+</project>
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 (file)
index 0000000..df4159c
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.luxom-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-luxom" description="Luxom Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.luxom/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..d974cd9
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..57ea4e7
--- /dev/null
@@ -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 (file)
index 0000000..ff16b48
--- /dev/null
@@ -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 (file)
index 0000000..c1e5e1c
--- /dev/null
@@ -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<List<CommandExecutionSpecification>> 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<CommandExecutionSpecification> 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<CommandExecutionSpecification> 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 (file)
index 0000000..1dc011a
--- /dev/null
@@ -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 (file)
index 0000000..e8d6db3
--- /dev/null
@@ -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<Integer> 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<CommandExecutionSpecification> 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 (file)
index 0000000..3b27fb1
--- /dev/null
@@ -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 (file)
index 0000000..cf82850
--- /dev/null
@@ -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<CommandExecutionSpecification> 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 (file)
index 0000000..52bbd5c
--- /dev/null
@@ -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 (file)
index 0000000..7a7de1a
--- /dev/null
@@ -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 (file)
index 0000000..84c25ea
--- /dev/null
@@ -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 (file)
index 0000000..965943e
--- /dev/null
@@ -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 (file)
index 0000000..bf7fba4
--- /dev/null
@@ -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 (file)
index 0000000..ce39802
--- /dev/null
@@ -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 (file)
index 0000000..9dc0a97
--- /dev/null
@@ -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:
+ * <ul>
+ * <li>Start and stop TCP socket connection with Luxom IP-interface.
+ * <li>Read all setup and status information from the Luxom Controller.
+ * <li>Execute Luxom commands.
+ * <li>Listen to events from Luxom.
+ * </ul>
+ *
+ * @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.
+     * <p>
+     * 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 (file)
index 0000000..7e20eab
--- /dev/null
@@ -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 (file)
index 0000000..8f0c1d8
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="luxom" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Luxom Binding</name>
+       <description>This is the binding for Luxom bus system (https://www.luxom.io/)</description>
+</binding:binding>
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 (file)
index 0000000..0b7aa47
--- /dev/null
@@ -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 (file)
index 0000000..58060d2
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="luxom"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="bridge">
+               <label>Luxom Bridge</label>
+               <description>This bridge represents a Luxom IP-interface (for example a DS-65L)</description>
+               <config-description>
+                       <parameter name="ipAddress" type="text" required="true">
+                               <context>network-address</context>
+                               <label>IP or Host Name</label>
+                               <description>The IP or host name of the Luxom IP-interface</description>
+                       </parameter>
+                       <parameter name="port" type="integer" required="true">
+                               <label>Bridge Port</label>
+                               <description>Port to communicate with Luxom IP-interface, default 2300</description>
+                               <default>2300</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="reconnectInterval" type="integer" min="1" max="60" unit="min">
+                               <label>Reconnect Interval</label>
+                               <description>The period in minutes that the handler will wait between connection attempts after disconnect</description>
+                               <unitLabel>minutes</unitLabel>
+                               <default>1</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="switch">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>switch</label>
+               <description>Luxom Switch</description>
+               <channels>
+                       <channel id="switch" typeId="switchState"/>
+               </channels>
+               <representation-property>address</representation-property>
+               <config-description>
+                       <parameter name="address" type="text" required="true">
+                               <label>Address</label>
+                               <description>Luxom bus address</description>
+                               <advanced>false</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+       <thing-type id="dimmer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Dimmer</label>
+               <description>Luxom Dimmer</description>
+               <channels>
+                       <channel id="brightness" typeId="system.brightness"/>
+               </channels>
+               <config-description>
+                       <parameter name="address" type="text" required="true">
+                               <label>Address</label>
+                               <description>Luxom bus address</description>
+                               <advanced>false</advanced>
+                       </parameter>
+                       <parameter name="onLevel" type="decimal" min="0.01" max="100.00">
+                               <label>On Level</label>
+                               <description>Output level to go to when an ON command is received. Default is 100%.</description>
+                               <default>100</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="onToLast" type="boolean">
+                               <label>Turn On To Last Level</label>
+                               <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.
+                               </description>
+                               <default>false</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="stepPercentage" type="integer" required="false">
+                               <label>Step Value</label>
+                               <description>Step value used for increase/decrease of dimmer brightness, default 5%</description>
+                               <default>5</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="switchState">
+               <item-type>Switch</item-type>
+               <label>Switch</label>
+               <description>Switch control for action in Luxom</description>
+               <category>Switch</category>
+               <autoUpdatePolicy>veto</autoUpdatePolicy>
+       </channel-type>
+</thing:thing-descriptions>
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 (file)
index 0000000..ddeb795
--- /dev/null
@@ -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 (file)
index 0000000..e404820
--- /dev/null
@@ -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));
+    }
+}
index 4e1d0c9a8d6e3bdc7c8ceba9dcdf77a829223fde..f2eb2e1bf0891959cb142d3034f204f6f5562a14 100644 (file)
     <module>org.openhab.binding.loxone</module>
     <module>org.openhab.binding.luftdateninfo</module>
     <module>org.openhab.binding.lutron</module>
+    <module>org.openhab.binding.luxom</module>
     <module>org.openhab.binding.luxtronikheatpump</module>
     <module>org.openhab.binding.magentatv</module>
     <module>org.openhab.binding.mail</module>