From ada54dbcfa7e93a433df39de15747ee423e0af4a Mon Sep 17 00:00:00 2001 From: Robert Schmid Date: Mon, 19 Apr 2021 19:51:50 +0200 Subject: [PATCH] [dali] Initial contribution (#10093) Signed-off-by: Robert Schmid --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.dali/NOTICE | 13 ++ bundles/org.openhab.binding.dali/README.md | 71 +++++++ bundles/org.openhab.binding.dali/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../dali/internal/DaliBindingConstants.java | 52 +++++ .../dali/internal/DaliHandlerFactory.java | 70 +++++++ .../internal/handler/DaliDeviceHandler.java | 146 ++++++++++++++ .../dali/internal/handler/DaliException.java | 42 ++++ .../dali/internal/handler/DaliRgbHandler.java | 170 ++++++++++++++++ .../handler/DaliserverBridgeHandler.java | 181 ++++++++++++++++++ .../internal/handler/DaliserverConfig.java | 26 +++ .../dali/internal/protocol/DaliAddress.java | 104 ++++++++++ .../internal/protocol/DaliBackwardFrame.java | 29 +++ .../internal/protocol/DaliCommandBase.java | 31 +++ .../internal/protocol/DaliDAPCCommand.java | 29 +++ .../internal/protocol/DaliForwardFrame.java | 29 +++ .../dali/internal/protocol/DaliFrame.java | 74 +++++++ .../protocol/DaliGearCommandBase.java | 28 +++ .../dali/internal/protocol/DaliResponse.java | 54 ++++++ .../protocol/DaliStandardCommand.java | 87 +++++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../main/resources/OH-INF/config/config.xml | 20 ++ .../resources/OH-INF/thing/thing-types.xml | 81 ++++++++ bundles/pom.xml | 1 + 26 files changed, 1379 insertions(+) create mode 100644 bundles/org.openhab.binding.dali/NOTICE create mode 100644 bundles/org.openhab.binding.dali/README.md create mode 100644 bundles/org.openhab.binding.dali/pom.xml create mode 100644 bundles/org.openhab.binding.dali/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliBindingConstants.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/DaliHandlerFactory.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliDeviceHandler.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliException.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliRgbHandler.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverBridgeHandler.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/handler/DaliserverConfig.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliAddress.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliBackwardFrame.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliCommandBase.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliDAPCCommand.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliForwardFrame.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliFrame.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliGearCommandBase.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliResponse.java create mode 100644 bundles/org.openhab.binding.dali/src/main/java/org/openhab/binding/dali/internal/protocol/DaliStandardCommand.java create mode 100644 bundles/org.openhab.binding.dali/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.dali/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.dali/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index f24b1241558..58bea852727 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 2b591c4aa9b..ae1c023e9c0 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 00000000000..38d625e3492 --- /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 00000000000..2c89809c0f2 --- /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 00000000000..ac94aebd006 --- /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 00000000000..430ab5a9f33 --- /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 00000000000..0eee45add2f --- /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 00000000000..2e9f573afac --- /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 00000000000..e75acf299b7 --- /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 00000000000..e2b360fdf71 --- /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 00000000000..2e0de931ce5 --- /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 00000000000..e5f0a3250b8 --- /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 00000000000..ea378b8983f --- /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 00000000000..082f9f6eb7f --- /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 00000000000..c2af5e9987f --- /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 00000000000..b3e429def89 --- /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 00000000000..fd56717a513 --- /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 00000000000..cb16ac6da21 --- /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 00000000000..ba6b714d841 --- /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 00000000000..5ac4f8defd2 --- /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 00000000000..6eea963c8d6 --- /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 00000000000..62f25620237 --- /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 00000000000..b2e7525c60b --- /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 00000000000..d2192e109d7 --- /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 00000000000..14da66f0f00 --- /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 58f7f8ead97..72196cd9187 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