[dali] Initial contribution (#10093)

Signed-off-by: Robert Schmid <r.schmid@outlook.com>
This commit is contained in:
Robert Schmid 2021-04-19 19:51:50 +02:00 committed by GitHub
parent 5e3717c22a
commit ada54dbcfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1379 additions and 0 deletions

View 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

View File

@ -236,6 +236,11 @@
<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>

View File

@ -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

View File

@ -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"}
```

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}
};
}
}

View File

@ -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 });
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>