diff --git a/CODEOWNERS b/CODEOWNERS
index f24b12415..58bea8527 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 2b591c4aa..ae1c023e9 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -236,6 +236,11 @@
org.openhab.binding.daikin
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.dali
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.danfossairunit
diff --git a/bundles/org.openhab.binding.dali/NOTICE b/bundles/org.openhab.binding.dali/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.dali/README.md b/bundles/org.openhab.binding.dali/README.md
new file mode 100644
index 000000000..2c89809c0
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/README.md
@@ -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
index 000000000..ac94aebd0
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.dali
+
+ openHAB Add-ons :: Bundles :: DALI Binding
+
+
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
index 000000000..430ab5a9f
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.dali/${project.version}
+
+
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
index 000000000..0eee45add
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliBindingConstants.java
@@ -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 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
index 000000000..2e9f573af
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliHandlerFactory.java
@@ -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 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
index 000000000..e75acf299
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliDeviceHandler.java
@@ -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
index 000000000..e2b360fdf
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliException.java
@@ -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
index 000000000..2e0de931c
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliRgbHandler.java
@@ -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 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
index 000000000..e5f0a3250
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverBridgeHandler.java
@@ -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 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 CompletableFuture<@Nullable T> sendCommandWithResponse(DaliCommandBase command,
+ Class 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 @Nullable T parseResponse(DataInputStream reader, Class 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
index 000000000..ea378b898
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverConfig.java
@@ -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
index 000000000..082f9f6eb
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliAddress.java
@@ -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 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 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 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 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 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
index 000000000..c2af5e998
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliBackwardFrame.java
@@ -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
index 000000000..b3e429def
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliCommandBase.java
@@ -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
index 000000000..fd56717a5
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliDAPCCommand.java
@@ -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
index 000000000..cb16ac6da
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliForwardFrame.java
@@ -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
index 000000000..ba6b714d8
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliFrame.java
@@ -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 bytesList = new ArrayList();
+ 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
index 000000000..5ac4f8def
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliGearCommandBase.java
@@ -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
index 000000000..6eea963c8
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliResponse.java
@@ -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
index 000000000..62f256202
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliStandardCommand.java
@@ -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
index 000000000..b2e7525c6
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ DALI Binding
+ This is the binding for controlling lights using the Digital Addressable Lighting Interface (DALI).
+
+
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
index 000000000..d2192e109
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ network-address
+ IP address or host name of daliserver.
+
+
+
+ Port of the daliserver TCP interface.
+ 55825
+
+
+
+
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
index 000000000..14da66f0f
--- /dev/null
+++ b/bundles/org.openhab.binding.dali/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+ A running daliserver.
+
+
+
+
+
+
+
+
+
+
+ Controls a single device/ballast
+
+
+
+
+
+
+
+
+ Address of the device in the DALI bus
+
+
+
+
+
+
+
+
+
+
+ Controls a group of devices/ballasts
+
+
+
+
+
+
+
+
+ Address of the group in the DALI bus
+
+
+
+
+
+
+
+
+
+
+ Controls three DALI devices representing R,G,B lighting channels
+
+
+
+
+
+
+
+ Address of the device in the DALI bus
+
+
+
+ Address of the device in the DALI bus
+
+
+
+ Address of the device in the DALI bus
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 58f7f8ead..72196cd91 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -82,6 +82,7 @@
org.openhab.binding.coolmasternet
org.openhab.binding.coronastats
org.openhab.binding.daikin
+ org.openhab.binding.dali
org.openhab.binding.danfossairunit
org.openhab.binding.darksky
org.openhab.binding.deconz