]> git.basschouten.com Git - openhab-addons.git/commitdiff
[dali] Initial contribution (#10093)
authorRobert Schmid <r.schmid@outlook.com>
Mon, 19 Apr 2021 17:51:50 +0000 (19:51 +0200)
committerGitHub <noreply@github.com>
Mon, 19 Apr 2021 17:51:50 +0000 (19:51 +0200)
Signed-off-by: Robert Schmid <r.schmid@outlook.com>
26 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.dali/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.dali/README.md [new file with mode: 0644]
bundles/org.openhab.binding.dali/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliException.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliRgbHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliAddress.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliBackwardFrame.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliCommandBase.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliDAPCCommand.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliForwardFrame.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliFrame.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliGearCommandBase.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliStandardCommand.java [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.dali/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index f24b124155899e61740775a4330fd69c06558340..58bea85272771a1e45218507d94d418e0b333436 100644 (file)
@@ -50,6 +50,7 @@
 /bundles/org.openhab.binding.coolmasternet/ @projectgus
 /bundles/org.openhab.binding.coronastats/ @DerOetzi
 /bundles/org.openhab.binding.daikin/ @caffineehacker
+/bundles/org.openhab.binding.dali/ @rs22
 /bundles/org.openhab.binding.danfossairunit/ @pravussum
 /bundles/org.openhab.binding.darksky/ @cweitkamp
 /bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers
index 2b591c4aa9b58481bb8e99444759d9cb0d285cef..ae1c023e9c0ad45cf85e9b9bc62dbce7fc60f3f2 100644 (file)
       <artifactId>org.openhab.binding.daikin</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.dali</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.danfossairunit</artifactId>
diff --git a/bundles/org.openhab.binding.dali/NOTICE b/bundles/org.openhab.binding.dali/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.dali/README.md b/bundles/org.openhab.binding.dali/README.md
new file mode 100644 (file)
index 0000000..2c89809
--- /dev/null
@@ -0,0 +1,71 @@
+# DALI Binding
+
+This binding supports controlling devices on a DALI bus (Digital Addressable Lighting Interface) via a [daliserver](https://github.com/onitake/daliserver) connection.
+
+Daliserver supports the Tridonic/Lunatone DALI USB adapter.
+As it only provides a thin multiplexer for the USB interface, the DALI messages themselves are implemented as part of this binding.
+
+## Supported Things
+
+Currently, these things are supported:
+
+ - daliserver (bridge)
+ - device (single device/ballast on the DALI bus)
+ - group (group of DALI devices)
+ - rgb (virtual device consisting of three directly addressed devices that represent r/g/b (LED) color channels)
+This binding was tested on a DALI 1 bus with daliserver 0.2.
+
+## Discovery
+
+Automatic device discovery is not yet implemented.
+
+## Thing Configuration
+
+### Bridge `daliserver`
+
+| Parameter   | Parameter ID | Required/Optional |  description                           |
+|-------------|--------------|-------------------|----------------------------------------|
+| Hostname    | host         | Required          | IP address or host name of daliserver  |
+| Port Number | port         | Required          | Port of the daliserver TCP interface   |
+
+### device
+
+| Parameter   | Parameter ID | Required/Optional |  description                           |
+|-------------|--------------|-------------------|----------------------------------------|
+| Device ID   | targetId     | Required          | Address of device in the DALI bus      |
+
+### group
+
+| Parameter   | Parameter ID | Required/Optional |  description                           |
+|-------------|--------------|-------------------|----------------------------------------|
+| Group  ID   | targetId     | Required          | Address of group in the DALI bus       |
+
+### rgb
+
+| Parameter   | Parameter ID | Required/Optional |  description                           |
+|-------------|--------------|-------------------|----------------------------------------|
+| R Device ID | targetIdR    | Required          | Address of device in the DALI bus      |
+| G Device ID | targetIdG    | Required          | Address of device in the DALI bus      |
+| B Device ID | targetIdB    | Required          | Address of device in the DALI bus      |
+
+## Full Example
+
+.things file
+
+```
+Bridge dali:daliserver:237dbae7 "Daliserver" [ host="localhost", port=55825] {
+    Thing rgb 87bf0403-a45d-4037-b874-28f4ece30004 "RGB Lights" [ targetIdR=0, targetIdG=1, targetIdB=2 ]
+    Thing device 995e16ca-07c4-4111-9cda-504cb5120f82 "Warm White" [ targetId=3 ]
+    Thing group 31da8dac-8e09-455a-bc7a-6ed70f740001 "Living Room Lights" [ targetId=0 ]
+}
+```
+
+
+.items file
+
+```
+Dimmer WarmWhiteLivingRoom "Warm White Living Room"  {channel="dali:device:237dbae7:995e16ca-07c4-4111-9cda-504cb5120f82:dimImmediately"}
+Color ColorLivingRoom "Light Color Living Room"  {channel="dali:device:237dbae7:87bf0403-a45d-4037-b874-28f4ece30004:color"}
+Switch LightsLivingRoom "Lights Living Room On/Off"  {channel="dali:device:237dbae7:31da8dac-8e09-455a-bc7a-6ed70f740001:dimImmediately"}
+```
diff --git a/bundles/org.openhab.binding.dali/pom.xml b/bundles/org.openhab.binding.dali/pom.xml
new file mode 100644 (file)
index 0000000..ac94aeb
--- /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.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.dali</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: DALI Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.dali/src/main/feature/feature.xml b/bundles/org.openhab.binding.dali/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..430ab5a
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.dali-${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-dali" description="DALI Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dali/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliBindingConstants.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliBindingConstants.java
new file mode 100644 (file)
index 0000000..0eee45a
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link DaliBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliBindingConstants {
+
+    private static final String BINDING_ID = "dali";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID BRIDGE_TYPE = new ThingTypeUID(BINDING_ID, "daliserver");
+    public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device");
+    public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
+    public static final ThingTypeUID THING_TYPE_RGB = new ThingTypeUID(BINDING_ID, "rgb");
+
+    public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = new HashSet<>(
+            Arrays.asList(THING_TYPE_DEVICE, THING_TYPE_GROUP, THING_TYPE_RGB));
+
+    public static final String CHANNEL_DIM_AT_FADE_RATE = "dimAtFadeRate";
+    public static final String CHANNEL_DIM_IMMEDIATELY = "dimImmediately";
+    public static final String CHANNEL_COLOR = "color";
+
+    public static final String TARGET_ID = "targetId";
+    public static final String TARGET_ID_R = "targetIdR";
+    public static final String TARGET_ID_G = "targetIdG";
+    public static final String TARGET_ID_B = "targetIdB";
+
+    public static final int DALI_SWITCH_100_PERCENT = 254;
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliHandlerFactory.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliHandlerFactory.java
new file mode 100644 (file)
index 0000000..2e9f573
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal;
+
+import static org.openhab.binding.dali.internal.DaliBindingConstants.*;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.dali.internal.handler.DaliDeviceHandler;
+import org.openhab.binding.dali.internal.handler.DaliRgbHandler;
+import org.openhab.binding.dali.internal.handler.DaliserverBridgeHandler;
+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 DaliHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.dali", service = ThingHandlerFactory.class)
+public class DaliHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
+            .concat(DaliserverBridgeHandler.SUPPORTED_THING_TYPES.stream(),
+                    DaliBindingConstants.SUPPORTED_DEVICE_THING_TYPES_UIDS.stream())
+            .collect(Collectors.toSet());
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (DaliserverBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
+            return new DaliserverBridgeHandler((Bridge) thing);
+        }
+        if (THING_TYPE_DEVICE.equals(thingTypeUID) || THING_TYPE_GROUP.equals(thingTypeUID)) {
+            return new DaliDeviceHandler(thing);
+        }
+        if (THING_TYPE_RGB.equals(thingTypeUID)) {
+            return new DaliRgbHandler(thing);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliDeviceHandler.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliDeviceHandler.java
new file mode 100644 (file)
index 0000000..e75acf2
--- /dev/null
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.handler;
+
+import static org.openhab.binding.dali.internal.DaliBindingConstants.*;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.dali.internal.protocol.DaliAddress;
+import org.openhab.binding.dali.internal.protocol.DaliDAPCCommand;
+import org.openhab.binding.dali.internal.protocol.DaliResponse;
+import org.openhab.binding.dali.internal.protocol.DaliStandardCommand;
+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.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link DaliDeviceHandler} handles commands for things of type Device and Group.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliDeviceHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(DaliDeviceHandler.class);
+    private @Nullable Integer targetId;
+
+    public DaliDeviceHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        Bridge bridge = getBridge();
+
+        if (bridge == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
+        } else {
+            updateStatus(ThingStatus.ONLINE);
+        }
+
+        targetId = ((BigDecimal) this.thing.getConfiguration().get(TARGET_ID)).intValueExact();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        try {
+            if (CHANNEL_DIM_AT_FADE_RATE.equals(channelUID.getId())
+                    || CHANNEL_DIM_IMMEDIATELY.equals(channelUID.getId())) {
+                DaliAddress address;
+                if (THING_TYPE_DEVICE.equals(this.thing.getThingTypeUID())) {
+                    address = DaliAddress.createShortAddress(targetId);
+                } else if (THING_TYPE_GROUP.equals(this.thing.getThingTypeUID())) {
+                    address = DaliAddress.createGroupAddress(targetId);
+                } else {
+                    throw new DaliException("unknown device type");
+                }
+
+                boolean queryDeviceState = false;
+
+                if (command instanceof PercentType) {
+                    byte dimmValue = (byte) ((((PercentType) command).floatValue() * DALI_SWITCH_100_PERCENT) / 100);
+                    // A dimm value of zero is handled correctly by DALI devices, i.e. they are turned off
+                    getBridgeHandler().sendCommand(new DaliDAPCCommand(address, dimmValue));
+                } else if (command instanceof OnOffType) {
+                    if ((OnOffType) command == OnOffType.ON) {
+                        getBridgeHandler().sendCommand(new DaliDAPCCommand(address, (byte) DALI_SWITCH_100_PERCENT));
+                    } else {
+                        getBridgeHandler().sendCommand(DaliStandardCommand.createOffCommand(address));
+                    }
+                } else if (command instanceof IncreaseDecreaseType) {
+                    if (CHANNEL_DIM_AT_FADE_RATE.equals(channelUID.getId())) {
+                        if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
+                            getBridgeHandler().sendCommand(DaliStandardCommand.createUpCommand(address));
+                        } else {
+                            getBridgeHandler().sendCommand(DaliStandardCommand.createDownCommand(address));
+                        }
+                    } else {
+                        if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
+                            getBridgeHandler().sendCommand(DaliStandardCommand.createStepUpCommand(address));
+                        } else {
+                            getBridgeHandler().sendCommand(DaliStandardCommand.createStepDownCommand(address));
+                        }
+                    }
+                    queryDeviceState = true;
+                } else if (command instanceof RefreshType) {
+                    queryDeviceState = true;
+                }
+
+                if (queryDeviceState) {
+                    getBridgeHandler()
+                            .sendCommandWithResponse(DaliStandardCommand.createQueryActualLevelCommand(address),
+                                    DaliResponse.NumericMask.class)
+                            .thenAccept(response -> {
+                                if (response != null && !response.mask) {
+                                    Integer value = response.value != null ? response.value : 0;
+                                    int percentValue = (int) (value.floatValue() * 100 / DALI_SWITCH_100_PERCENT);
+                                    updateState(channelUID, new PercentType(percentValue));
+                                }
+                            }).exceptionally(e -> {
+                                logger.warn("Error querying device status: {}", e.getMessage());
+                                return null;
+                            });
+                }
+            }
+        } catch (DaliException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        }
+    }
+
+    protected DaliserverBridgeHandler getBridgeHandler() throws DaliException {
+        Bridge bridge = this.getBridge();
+        if (bridge == null) {
+            throw new DaliException("No bridge was found");
+        }
+
+        BridgeHandler handler = bridge.getHandler();
+        if (handler == null) {
+            throw new DaliException("No handler was found");
+        }
+
+        return (DaliserverBridgeHandler) handler;
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliException.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliException.java
new file mode 100644 (file)
index 0000000..e2b360f
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link DaliException} signals exceptions within the DALI binding
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public DaliException() {
+        super();
+    }
+
+    public DaliException(String message) {
+        super(message);
+    }
+
+    public DaliException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DaliException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliRgbHandler.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliRgbHandler.java
new file mode 100644 (file)
index 0000000..2e0de93
--- /dev/null
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.handler;
+
+import static org.openhab.binding.dali.internal.DaliBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.dali.internal.protocol.DaliAddress;
+import org.openhab.binding.dali.internal.protocol.DaliDAPCCommand;
+import org.openhab.binding.dali.internal.protocol.DaliResponse;
+import org.openhab.binding.dali.internal.protocol.DaliResponse.NumericMask;
+import org.openhab.binding.dali.internal.protocol.DaliStandardCommand;
+import org.openhab.core.library.types.HSBType;
+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.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link DaliRgbHandler} handles commands for things of type RGB.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliRgbHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(DaliRgbHandler.class);
+    private @Nullable List<Integer> outputs;
+
+    public DaliRgbHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        Bridge bridge = getBridge();
+
+        if (bridge == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
+        } else {
+            updateStatus(ThingStatus.ONLINE);
+        }
+
+        outputs = List.of(((BigDecimal) this.thing.getConfiguration().get(TARGET_ID_R)).intValueExact(),
+                ((BigDecimal) this.thing.getConfiguration().get(TARGET_ID_G)).intValueExact(),
+                ((BigDecimal) this.thing.getConfiguration().get(TARGET_ID_B)).intValueExact());
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        try {
+            if (CHANNEL_COLOR.equals(channelUID.getId())) {
+                boolean queryDeviceState = false;
+
+                if (command instanceof HSBType) {
+                    PercentType[] rgb = ((HSBType) command).toRGB();
+
+                    for (int i = 0; i < 3; i++) {
+                        byte dimmValue = (byte) ((rgb[i].floatValue() * DALI_SWITCH_100_PERCENT) / 100);
+                        getBridgeHandler().sendCommand(
+                                new DaliDAPCCommand(DaliAddress.createShortAddress(outputs.get(i)), dimmValue));
+                    }
+                } else if (command instanceof OnOffType) {
+                    if ((OnOffType) command == OnOffType.ON) {
+                        for (Integer output : outputs) {
+                            getBridgeHandler().sendCommand(new DaliDAPCCommand(DaliAddress.createShortAddress(output),
+                                    (byte) DALI_SWITCH_100_PERCENT));
+                        }
+                    } else {
+                        for (Integer output : outputs) {
+                            getBridgeHandler().sendCommand(
+                                    DaliStandardCommand.createOffCommand(DaliAddress.createShortAddress(output)));
+                        }
+                    }
+                } else if (command instanceof IncreaseDecreaseType) {
+                    if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
+                        for (Integer output : outputs) {
+                            getBridgeHandler().sendCommand(
+                                    DaliStandardCommand.createUpCommand(DaliAddress.createShortAddress(output)));
+                        }
+                    } else {
+                        for (Integer output : outputs) {
+                            getBridgeHandler().sendCommand(
+                                    DaliStandardCommand.createDownCommand(DaliAddress.createShortAddress(output)));
+                        }
+                    }
+
+                    queryDeviceState = true;
+                } else if (command instanceof RefreshType) {
+                    queryDeviceState = true;
+                }
+
+                if (queryDeviceState) {
+                    CompletableFuture<@Nullable NumericMask> responseR = getBridgeHandler()
+                            .sendCommandWithResponse(
+                                    DaliStandardCommand.createQueryActualLevelCommand(
+                                            DaliAddress.createShortAddress(outputs.get(0))),
+                                    DaliResponse.NumericMask.class);
+                    CompletableFuture<@Nullable NumericMask> responseG = getBridgeHandler()
+                            .sendCommandWithResponse(
+                                    DaliStandardCommand.createQueryActualLevelCommand(
+                                            DaliAddress.createShortAddress(outputs.get(1))),
+                                    DaliResponse.NumericMask.class);
+                    CompletableFuture<@Nullable NumericMask> responseB = getBridgeHandler()
+                            .sendCommandWithResponse(
+                                    DaliStandardCommand.createQueryActualLevelCommand(
+                                            DaliAddress.createShortAddress(outputs.get(2))),
+                                    DaliResponse.NumericMask.class);
+
+                    CompletableFuture.allOf(responseR, responseG, responseB).thenAccept(x -> {
+                        @Nullable
+                        NumericMask r = responseR.join(), g = responseG.join(), b = responseB.join();
+                        if (r != null && !r.mask && g != null && !g.mask && b != null && !b.mask) {
+                            Integer rValue = r.value != null ? r.value : 0;
+                            Integer gValue = g.value != null ? g.value : 0;
+                            Integer bValue = b.value != null ? b.value : 0;
+                            updateState(channelUID,
+                                    HSBType.fromRGB((int) (rValue.floatValue() * 255 / DALI_SWITCH_100_PERCENT),
+                                            (int) (gValue.floatValue() * 255 / DALI_SWITCH_100_PERCENT),
+                                            (int) (bValue.floatValue() * 255 / DALI_SWITCH_100_PERCENT)));
+                        }
+                    }).exceptionally(e -> {
+                        logger.warn("Error querying device status: {}", e.getMessage());
+                        return null;
+                    });
+                }
+            }
+        } catch (DaliException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        }
+    }
+
+    protected DaliserverBridgeHandler getBridgeHandler() throws DaliException {
+        Bridge bridge = this.getBridge();
+        if (bridge == null) {
+            throw new DaliException("No bridge was found");
+        }
+
+        BridgeHandler handler = bridge.getHandler();
+        if (handler == null) {
+            throw new DaliException("No handler was found");
+        }
+
+        return (DaliserverBridgeHandler) handler;
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverBridgeHandler.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverBridgeHandler.java
new file mode 100644 (file)
index 0000000..e5f0a32
--- /dev/null
@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.handler;
+
+import static org.openhab.binding.dali.internal.DaliBindingConstants.*;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.dali.internal.protocol.DaliBackwardFrame;
+import org.openhab.binding.dali.internal.protocol.DaliCommandBase;
+import org.openhab.binding.dali.internal.protocol.DaliResponse;
+import org.openhab.core.common.NamedThreadFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link DaliserverBridgeHandler} handles the lifecycle of daliserver connections.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliserverBridgeHandler extends BaseBridgeHandler {
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
+
+    private final Logger logger = LoggerFactory.getLogger(DaliserverBridgeHandler.class);
+    private static final int DALI_DEFAULT_TIMEOUT = 5000;
+
+    private DaliserverConfig config = new DaliserverConfig();
+    private @Nullable ExecutorService commandExecutor;
+
+    public DaliserverBridgeHandler(Bridge thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(DaliserverConfig.class);
+        commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    private Socket getConnection() throws IOException {
+        try {
+            logger.debug("Creating connection to daliserver on: {}  port: {}", config.host, config.port);
+            Socket socket = new Socket(config.host, config.port);
+            socket.setSoTimeout(DALI_DEFAULT_TIMEOUT);
+            return socket;
+        } catch (IOException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            throw e;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        if (commandExecutor != null) {
+            commandExecutor.shutdownNow();
+        }
+    }
+
+    public CompletableFuture<@Nullable Void> sendCommand(DaliCommandBase command) {
+        return sendCommandWithResponse(command, DaliResponse.class).thenApply(c -> (Void) null);
+    }
+
+    public <T extends DaliResponse> CompletableFuture<@Nullable T> sendCommandWithResponse(DaliCommandBase command,
+            Class<T> responseType) {
+        CompletableFuture<@Nullable T> future = new CompletableFuture<>();
+        ExecutorService commandExecutor = this.commandExecutor;
+        if (commandExecutor != null) {
+            commandExecutor.submit(() -> {
+                byte[] prefix = new byte[] { 0x2, 0x0 };
+                byte[] message = command.frame.pack();
+                byte[] frame = new byte[prefix.length + message.length];
+                System.arraycopy(prefix, 0, frame, 0, prefix.length);
+                System.arraycopy(message, 0, frame, prefix.length, message.length);
+
+                try (Socket socket = getConnection();
+                        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+                        DataInputStream in = new DataInputStream(socket.getInputStream())) {
+                    // send the command
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Sending: {}", HexUtils.bytesToHex(frame));
+                    }
+                    out.write(frame);
+                    if (command.sendTwice) {
+                        out.flush();
+                        in.readNBytes(4); // discard
+                        out.write(frame);
+                    }
+                    out.flush();
+
+                    // read the response
+                    try {
+                        @Nullable
+                        T response = parseResponse(in, responseType);
+                        future.complete(response);
+                        return;
+                    } catch (DaliException e) {
+                        future.completeExceptionally(e);
+                        return;
+                    }
+                } catch (SocketTimeoutException e) {
+                    logger.warn("Timeout sending command to daliserver: {} Message: {}", frame, e.getMessage());
+                    future.completeExceptionally(new DaliException("Timeout sending command to daliserver", e));
+                } catch (IOException e) {
+                    logger.warn("Problem sending command to daliserver: {} Message: {}", frame, e.getMessage());
+                    future.completeExceptionally(new DaliException("Problem sending command to daliserver", e));
+                } catch (Exception e) {
+                    logger.warn("Unexpected exception while sending command to daliserver: {} Message: {}", frame,
+                            e.getMessage());
+                    future.completeExceptionally(e);
+                }
+            });
+        } else {
+            future.complete(null);
+        }
+        return future;
+    }
+
+    private <T extends DaliResponse> @Nullable T parseResponse(DataInputStream reader, Class<T> responseType)
+            throws IOException, DaliException {
+        try {
+            T result = responseType.getDeclaredConstructor().newInstance();
+            byte[] response = reader.readNBytes(4);
+            if (logger.isDebugEnabled()) {
+                logger.debug("Received: {}", HexUtils.bytesToHex(response));
+            }
+            byte status = response[1], rval = response[2];
+            if (status == 0) {
+                result.parse(null);
+            } else if (status == 1) {
+                result.parse(new DaliBackwardFrame(rval));
+            } else if (status == 255) {
+                // This is "failure" - daliserver reports this for a garbled response when several ballasts reply. It
+                // should be interpreted as "Yes".
+                result.parse(null);
+            } else {
+                throw new DaliException("Invalid response status: " + status);
+            }
+
+            return result;
+        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
+                | InvocationTargetException e) {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverConfig.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverConfig.java
new file mode 100644 (file)
index 0000000..ea378b8
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link DaliserverConfig} holds connection parameters for a daliserver instance.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliserverConfig {
+    public String host = "";
+    public int port = 0;
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliAddress.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliAddress.java
new file mode 100644 (file)
index 0000000..082f9f6
--- /dev/null
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliAddress} represents an address on the DALI bus.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public abstract class DaliAddress {
+    private DaliAddress() {
+    }
+
+    protected abstract <T extends DaliFrame> T addToFrame(T frame) throws DaliException;
+
+    public static DaliAddress createShortAddress(int address) throws DaliException {
+        if (address < 0 || address > 63) {
+            throw new DaliException("address must be in the range 0..63");
+        }
+        return new DaliAddress() {
+            @Override
+            protected <T extends DaliFrame> T addToFrame(T frame) throws DaliException {
+                if (frame.length() == 16) {
+                    frame.data &= ~(1 << 15); // unset bit 15
+                    frame.data |= ((address & 0b11111) << 9);
+                } else if (frame.length() == 24) {
+                    frame.data &= ~(1 << 23); // unset bit 23
+                    frame.data |= ((address & 0b11111) << 17);
+                } else {
+                    throw new DaliException("Unsupported frame size");
+                }
+                return frame;
+            }
+        };
+    }
+
+    public static DaliAddress createBroadcastAddress() {
+        return new DaliAddress() {
+            @Override
+            protected <T extends DaliFrame> T addToFrame(T frame) throws DaliException {
+                if (frame.length() == 16) {
+                    frame.data |= 0x7f << 9;
+                } else if (frame.length() == 24) {
+                    frame.data |= 0x7f << 17;
+                } else {
+                    throw new DaliException("Unsupported frame size");
+                }
+                return frame;
+            }
+        };
+    }
+
+    public static DaliAddress createBroadcastUnaddressedAddress() {
+        return new DaliAddress() {
+            @Override
+            protected <T extends DaliFrame> T addToFrame(T frame) throws DaliException {
+                if (frame.length() == 16) {
+                    frame.data |= 0x7e << 9;
+                } else if (frame.length() == 24) {
+                    frame.data |= 0x7e << 17;
+                } else {
+                    throw new DaliException("Unsupported frame size");
+                }
+                return frame;
+            }
+        };
+    }
+
+    public static DaliAddress createGroupAddress(int address) throws DaliException {
+        if (address < 0 || address > 31) {
+            throw new DaliException("address must be in the range 0..31");
+        }
+        return new DaliAddress() {
+            @Override
+            protected <T extends DaliFrame> T addToFrame(T frame) throws DaliException {
+                if (frame.length() == 16) {
+                    if (address > 15) {
+                        throw new DaliException("Groups 16..31 are not supported in 16-bit forward frames");
+                    }
+                    frame.data |= ((0x4 << 3) & (address & 0b111)) << 9;
+                } else if (frame.length() == 24) {
+                    frame.data |= ((0x2 << 4) & (address & 0b1111)) << 17;
+                } else {
+                    throw new DaliException("Unsupported frame size");
+                }
+                return frame;
+            }
+        };
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliBackwardFrame.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliBackwardFrame.java
new file mode 100644 (file)
index 0000000..c2af5e9
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliBackwardFrame} represents a response message on the DALI bus.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliBackwardFrame extends DaliFrame {
+
+    public DaliBackwardFrame(byte data) throws DaliException {
+        super(8, new byte[] { data });
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliCommandBase.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliCommandBase.java
new file mode 100644 (file)
index 0000000..b3e429d
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link DaliCommandBase} is an abstract command for DALI devices.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliCommandBase {
+    public DaliForwardFrame frame;
+    public boolean sendTwice;
+
+    public DaliCommandBase(DaliForwardFrame frame, boolean sendTwice) {
+        this.frame = frame;
+        this.sendTwice = sendTwice;
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliDAPCCommand.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliDAPCCommand.java
new file mode 100644 (file)
index 0000000..fd56717
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliDAPCCommand} represents a DALI Direct Arc Power Command.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliDAPCCommand extends DaliGearCommandBase {
+
+    public DaliDAPCCommand(DaliAddress target, Byte power) throws DaliException {
+        super(target.addToFrame(new DaliForwardFrame(16, new byte[] { power })), false);
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliForwardFrame.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliForwardFrame.java
new file mode 100644 (file)
index 0000000..cb16ac6
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliForwardFrame} represents an outgoing DALI command.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliForwardFrame extends DaliFrame {
+
+    public DaliForwardFrame(int bits, byte[] data) throws DaliException {
+        super(bits, data);
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliFrame.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliFrame.java
new file mode 100644 (file)
index 0000000..ba6b714
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliFrame} represents a message on the DALI bus.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliFrame {
+    int bits;
+    int data;
+
+    public DaliFrame(int bits, byte[] data) throws DaliException {
+        if (bits < 1) {
+            throw new DaliException("Frames must contain at least 1 data bit");
+        }
+
+        this.bits = bits;
+
+        int d = 0;
+        for (byte b : data) {
+            d = (d << 8) | Byte.toUnsignedInt(b);
+        }
+
+        this.data = d;
+
+        if (this.data < 0) {
+            throw new DaliException("Initial data must not be negative");
+        }
+
+        if (Math.abs(this.data) >= (1 << this.bits)) {
+            throw new DaliException("Initial data will not fit in the specified number of bits");
+        }
+    }
+
+    public int length() {
+        return this.bits;
+    }
+
+    public byte[] pack() {
+        int remaining = length();
+        List<Byte> bytesList = new ArrayList<Byte>();
+        int tmp = this.data;
+        while (remaining > 0) {
+            bytesList.add((byte) (tmp & 0xff));
+            tmp = tmp >> 8;
+            remaining = remaining - 8;
+        }
+        byte[] result = new byte[bytesList.size()];
+        int i = 0;
+        for (byte b : bytesList) {
+            result[bytesList.size() - i++] = b;
+        }
+        return result;
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliGearCommandBase.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliGearCommandBase.java
new file mode 100644 (file)
index 0000000..5ac4f8d
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link DaliGearCommandBase} represents an abstract DALI gear command.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliGearCommandBase extends DaliCommandBase {
+
+    public DaliGearCommandBase(DaliForwardFrame frame, boolean sendTwice) {
+        super(frame, sendTwice);
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliResponse.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliResponse.java
new file mode 100644 (file)
index 0000000..6eea963
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link DaliResponse} represents different types of responses to DALI
+ * commands.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliResponse {
+    public void parse(@Nullable DaliBackwardFrame frame) {
+    }
+
+    public static class Numeric extends DaliResponse {
+        public @Nullable Integer value;
+
+        @Override
+        public void parse(@Nullable DaliBackwardFrame frame) {
+            if (frame != null) {
+                value = frame.data;
+            }
+        }
+    }
+
+    public static class NumericMask extends DaliResponse.Numeric {
+        public @Nullable Boolean mask;
+
+        @Override
+        public void parse(@Nullable DaliBackwardFrame frame) {
+            super.parse(frame);
+            if (this.value == 255) {
+                this.value = null;
+                this.mask = true;
+            } else {
+                this.mask = false;
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliStandardCommand.java b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliStandardCommand.java
new file mode 100644 (file)
index 0000000..62f2562
--- /dev/null
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2010-2021 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.dali.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.dali.internal.handler.DaliException;
+
+/**
+ * The {@link DaliStandardCommand} represents different types of commands for
+ * controlling DALI equipment.
+ *
+ * @author Robert Schmid - Initial contribution
+ */
+@NonNullByDefault
+public class DaliStandardCommand extends DaliGearCommandBase {
+
+    private DaliStandardCommand(DaliAddress target, int cmdval, int param, boolean sendTwice) throws DaliException {
+        super(target.addToFrame(new DaliForwardFrame(16, new byte[] { 0x1, (byte) (cmdval | (param & 0b1111)) })),
+                sendTwice);
+    }
+
+    public static DaliStandardCommand createOffCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x00, 0, false);
+    }
+
+    public static DaliStandardCommand createUpCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x01, 0, false);
+    }
+
+    public static DaliStandardCommand createDownCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x02, 0, false);
+    }
+
+    public static DaliStandardCommand createStepUpCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x03, 0, false);
+    }
+
+    public static DaliStandardCommand createStepDownCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x04, 0, false);
+    }
+
+    public static DaliStandardCommand createRecallMaxLevelCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x05, 0, false);
+    }
+
+    public static DaliStandardCommand createRecallMinLevelCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x06, 0, false);
+    }
+
+    public static DaliStandardCommand createStepDownAndOffCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x07, 0, false);
+    }
+
+    public static DaliStandardCommand createOnAndStepUpCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x08, 0, false);
+    }
+
+    public static DaliStandardCommand createEnableDAPCSequenceCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x09, 0, false);
+    }
+
+    public static DaliStandardCommand createGoToSceneCommand(DaliAddress target, int scene) throws DaliException {
+        return new DaliStandardCommand(target, 0x10, scene, false);
+    }
+
+    public static DaliStandardCommand createResetCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x20, 0, true);
+    }
+
+    public static DaliStandardCommand createQueryStatusCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0x90, 0, false);
+    }
+
+    public static DaliStandardCommand createQueryActualLevelCommand(DaliAddress target) throws DaliException {
+        return new DaliStandardCommand(target, 0xa0, 0, false);
+    }
+}
diff --git a/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..b2e7525
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="dali" 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>DALI Binding</name>
+       <description>This is the binding for controlling lights using the Digital Addressable Lighting Interface (DALI).</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..d2192e1
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:dali:daliserver">
+               <parameter name="host" type="text" required="true">
+                       <label>Host Address</label>
+                       <context>network-address</context>
+                       <description>IP address or host name of daliserver.</description>
+               </parameter>
+               <parameter name="port" type="integer" required="true" min="1" max="65535">
+                       <label>TCP Port</label>
+                       <description>Port of the daliserver TCP interface.</description>
+                       <default>55825</default>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..14da66f
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="dali"
+       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="daliserver">
+               <label>Daliserver</label>
+               <description>A running daliserver.</description>
+
+               <config-description-ref uri="thing-type:dali:daliserver"/>
+       </bridge-type>
+
+       <!-- Single Device Type -->
+       <thing-type id="device">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="daliserver"/>
+               </supported-bridge-type-refs>
+               <label>DALI Device</label>
+               <description>Controls a single device/ballast</description>
+               <channels>
+                       <channel id="dimAtFadeRate" typeId="system.brightness"/>
+                       <channel id="dimImmediately" typeId="system.brightness"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="targetId" type="integer" required="true" min="0" max="63">
+                               <label>Device ID</label>
+                               <description>Address of the device in the DALI bus</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Group Device Type -->
+       <thing-type id="group">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="daliserver"/>
+               </supported-bridge-type-refs>
+               <label>DALI Group</label>
+               <description>Controls a group of devices/ballasts</description>
+               <channels>
+                       <channel id="dimAtFadeRate" typeId="system.brightness"/>
+                       <channel id="dimImmediately" typeId="system.brightness"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="targetId" type="integer" required="true" min="0" max="31">
+                               <label>Group ID</label>
+                               <description>Address of the group in the DALI bus</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- RGB Type -->
+       <thing-type id="rgb">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="daliserver"/>
+               </supported-bridge-type-refs>
+               <label>DALI RGB Device</label>
+               <description>Controls three DALI devices representing R,G,B lighting channels</description>
+               <channels>
+                       <channel id="color" typeId="system.color"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="targetIdR" type="integer" required="true" min="0" max="63">
+                               <label>R Device ID</label>
+                               <description>Address of the device in the DALI bus</description>
+                       </parameter>
+                       <parameter name="targetIdG" type="integer" required="true" min="0" max="63">
+                               <label>G Device ID</label>
+                               <description>Address of the device in the DALI bus</description>
+                       </parameter>
+                       <parameter name="targetIdB" type="integer" required="true" min="0" max="63">
+                               <label>B Device ID</label>
+                               <description>Address of the device in the DALI bus</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+</thing:thing-descriptions>
index 58f7f8ead97096c493717d1ad81392c75672cfcd..72196cd918725944b5fa5cbbe1fc3aadb5bf011c 100644 (file)
@@ -82,6 +82,7 @@
     <module>org.openhab.binding.coolmasternet</module>
     <module>org.openhab.binding.coronastats</module>
     <module>org.openhab.binding.daikin</module>
+    <module>org.openhab.binding.dali</module>
     <module>org.openhab.binding.danfossairunit</module>
     <module>org.openhab.binding.darksky</module>
     <module>org.openhab.binding.deconz</module>