From aae63e9488bf2b4233da9fbedc19f41bc97a3df4 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Fri, 17 Mar 2023 12:50:13 +0100 Subject: [PATCH] [knx] Refactoring of KnxCoreTypeMapper and UOM Support (#14534) * [knx] Refactoring, add basic support for UOM Preparation for refactoring KnxCoreTypeMapper. Carryover from smarthomej/addons#107. Merge UOM implementations. * [knx] Adapt tests DPT strings for QuantityType now strip off a tailing .0 when decimals are converted. * [knx] Refactoring Use pattern matching with instanceof operator (new Java17 feature). * [knx] Refactoring, performance improvements Introduce KNXChannel class. Carryover from smarthomej/addons#114. * [knx] Add warning for incompatible DPT type Configuring incompatible DPT/channel combinations (e.g. DPT 1.005 (alarm) on Contact channels or DPT 1.019 (windows/door) on Switch channels) is not allowed but was silently ignored. This PR adds a warning in case incompatible configurations are detected. Carryover from smarthomej/addons#203. * [knx] Add full support for UoM Replace UoM handling with the implementation from smarthome/j. Carryover from smarthomej/addons#206. * [knx] Refactor KNXCoreTypeMapper, add RGBW and xyY Carryover from smarthomej/addons#208. * [knx] Fix RGB conversion Carryover from smarthomej/addons#219. * [knx] Remove workarounds obsoleted by Calimero 2.5 Carryover from smarthomej/addons#226. * [knx] Add parameter for disabling incoming UoM Carryover from smarthomej/addons#230. * [knx] Fix fallback to DecimalType in number conversion Carryover from smarthomej/addons#279. * [knx] Fix DPT 251.600 decoding Carryover from smarthomej/addons#349. * [knx] Fix UoM handling for special types * [knx] Add test for KNXChannelFactory * [knx] Update CODEOWNERS for knx * [knx] Default conversion for DPT 5.001 and 6.001 * [knx] Fix write blocked forever after read from bus Carryover from smarthomej/addons#299 and smarthomej/addons#330. * [knx] Use new class ColorUtil from core for HSB conversion Also-by: Jan N. Klug Signed-off-by: Holger Friedrich --- CODEOWNERS | 2 +- bundles/org.openhab.binding.knx/README.md | 145 +- .../knx/internal/KNXBindingConstants.java | 13 +- .../knx/internal/channel/AbstractSpec.java | 66 - .../channel/ChannelConfiguration.java | 58 - .../channel/GroupAddressConfiguration.java | 107 +- .../knx/internal/channel/KNXChannel.java | 154 +++ .../internal/channel/KNXChannelFactory.java | 61 + .../knx/internal/channel/KNXChannelType.java | 218 --- .../knx/internal/channel/KNXChannelTypes.java | 59 - .../knx/internal/channel/ListenSpecImpl.java | 32 +- .../internal/channel/ReadRequestSpecImpl.java | 30 +- .../channel/ReadResponseSpecImpl.java | 39 +- .../knx/internal/channel/TypeColor.java | 21 +- .../knx/internal/channel/TypeContact.java | 16 +- .../knx/internal/channel/TypeDateTime.java | 16 +- .../knx/internal/channel/TypeDimmer.java | 18 +- .../knx/internal/channel/TypeNumber.java | 17 +- .../internal/channel/TypeRollershutter.java | 19 +- .../knx/internal/channel/TypeString.java | 16 +- .../knx/internal/channel/TypeSwitch.java | 16 +- .../knx/internal/channel/WriteSpecImpl.java | 41 +- .../internal/client/AbstractKNXClient.java | 93 +- .../internal/client/BusMessageListener.java | 7 +- .../knx/internal/client/InboundSpec.java | 4 +- .../knx/internal/client/KNXClient.java | 6 +- .../knx/internal/client/NoOpClient.java | 6 +- .../knx/internal/client/OutboundSpec.java | 12 +- .../internal/client/StatusUpdateCallback.java | 18 +- .../internal/config/BridgeConfiguration.java | 2 +- .../knx/internal/config/DeviceConfig.java | 1 - .../binding/knx/internal/dpt/DPTUnits.java | 155 +++ .../binding/knx/internal/dpt/DPTUtil.java | 127 ++ .../knx/internal/dpt/KNXCoreTypeMapper.java | 1174 ----------------- .../knx/internal/dpt/ValueDecoder.java | 382 ++++++ .../knx/internal/dpt/ValueEncoder.java | 255 ++++ .../internal/factory/KNXHandlerFactory.java | 12 +- .../handler/AbstractKNXThingHandler.java | 226 ---- .../internal/handler/DeviceThingHandler.java | 527 +++++--- .../src/main/resources/OH-INF/addon/addon.xml | 8 + .../channel/KNXChannelFactoryTest.java | 77 ++ .../knx/internal/channel/KNXChannelTest.java | 170 +++ .../internal/channel/KNXChannelTypeTest.java | 146 -- .../binding/knx/internal/dpt/DPTTest.java | 411 ++++++ .../internal/dpt/KNXCoreTypeMapperTest.java | 279 ---- 45 files changed, 2505 insertions(+), 2757 deletions(-) delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ChannelConfiguration.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannel.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelFactory.java delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelType.java delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelTypes.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUnits.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUtil.java delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueDecoder.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueEncoder.java delete mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java create mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelFactoryTest.java create mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTest.java delete mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTypeTest.java create mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/DPTTest.java delete mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapperTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 69fc52c11ce..210554db57d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -159,7 +159,7 @@ /bundles/org.openhab.binding.kaleidescape/ @mlobstein /bundles/org.openhab.binding.keba/ @kgoderis /bundles/org.openhab.binding.km200/ @Markinus -/bundles/org.openhab.binding.knx/ @kaikreuzer +/bundles/org.openhab.binding.knx/ @kaikreuzer @holgerfriedrich /bundles/org.openhab.binding.kodi/ @pail23 @cweitkamp /bundles/org.openhab.binding.konnected/ @volfan6415 /bundles/org.openhab.binding.kostalinverter/ @cschneider diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index 21b62a106ad..3b5348d5fb6 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -9,6 +9,18 @@ The KNX binding then can communicate directly with this gateway. Alternatively, a PC running [KNXD](https://github.com/knxd/knxd) (free open source component software) can be put in between which then acts as a broker allowing multiple client to connect to the same gateway. Since the protocol is identical, the KNX binding can also communicate with it transparently. +***Attention:*** With the introduction of Unit of Measurement (UoM) support, some data types have changed (see `number` channel below): + +- Data type for DPT 5.001 (Percent 8bit, 0 -> 100%) has changed from `PercentType` to `QuantityType`for `number` channels (`dimmer`, `color`, `rollershutter` channels stay with `PercentType`). +- Data type for DPT 5.004 (Percent 8bit, 0 -> 255%) has changed from `PercentType` to `QuantityType`. +- Data type for DPT 6.001 (Percent 8bit -128 -> 127%) has changed from `PercentType` to `QuantityType`. +- Data type for DPT 9.007 (Humidity) has changed from `PercentType` to `QuantityType`. + +Rules that check for or compare states and transformations that expect a raw value might need adjustments. +If you run into trouble with that and need some time, you can disable UoM support on binding level via the `disableUoM` parameter. +UoM are enabled by default and need to be disabled manually. +A new setting is activated immediately without restart. + ## Supported Things The KNX binding supports two types of bridges, and one type of things to access the KNX bus. @@ -16,7 +28,8 @@ There is an _ip_ bridge to connect to KNX IP Gateways, and a _serial_ bridge for ## Bridges -The following two bridge types are supported. Bridges don't have channels on their own. +The following two bridge types are supported. +Bridges don't have channels on their own. ### IP Gateway @@ -76,45 +89,30 @@ All channels of a device share one configuration parameter defined on device lev All readable group addresses are queried by openHAB during startup. If readInterval is not specified or set to 0, no further periodic reading will be triggered (default: 0). -#### Standard Channel Types +#### Channel Types Standard channels are used most of the time. They are used in the common case where the physical state is owned by a device within the KNX bus, e.g. by a switch actuator who "knows" whether the light is turned on or off, or by a temperature sensor which reports the room temperature regularly. -Note: After changing the DPT of already existing Channels, openHAB needs to be restarted for the changes to become effective. +Control channel types (suffix `-control`) are used for cases where the KNX bus does not own the physical state of a device. +This could be the case if e.g. a lamp from another binding should be controlled by a KNX wall switch. +When a `GroupValueRead` telegram is sent from the KNX bus to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus. -##### Channel Type "switch" - -| Parameter | Description | Default DPT | -|-----------|-------------------------------------|-------------| -| ga | Group address for the binary switch | 1.001 | - -##### Channel Type "dimmer" +##### Channel Type `color`, `color-control` | Parameter | Description | Default DPT | |------------------|----------------------------------------|-------------| +| hsb | Group address for the color | 232.600 | | switch | Group address for the binary switch | 1.001 | -| position | Group address of the absolute position | 5.001 | -| increaseDecrease | Group address for relative movement | 3.007 | +| position | Group address brightness | 5.001 | +| increaseDecrease | Group address for relative brightness | 3.007 | -##### Channel Type "color" +The `hsb` address supports DPT 242.600 and 251.600. -| Parameter | Description | Default DPT | -|------------------|----------------------------------------|-------------| -| hsb | Group address for color | 232.600 | -| switch | Group address for the binary switch | 1.001 | -| position | Group address of the absolute position | 5.001 | -| increaseDecrease | Group address for relative movement | 3.007 | +Some RGB/RGBW products (e.g. MDT) support HSB values for DPT 232.600 instead of RGB. +This is supported as "vendor-specific DPT" with a value of 232.60000. -##### Channel Type "rollershutter" - -| Parameter | Description | Default DPT | -|-----------|-----------------------------------------|-------------| -| upDown | Group address for relative movement | 1.008 | -| stopMove | Group address for stopping | 1.010 | -| position | Group address for the absolute position | 5.001 | - -##### Channel Type "contact" +##### Channel Type `contact`, `contact-control` | Parameter | Description | Default DPT | |-----------|---------------|-------------| @@ -123,32 +121,63 @@ Note: After changing the DPT of already existing Channels, openHAB needs to be r *Attention:* Due to a bug in the original implementation, the states for DPT 1.009 are inverted (i.e. `1` is mapped to `OPEN` instead of `CLOSE`). A change would break all existing installations and is therefore not implemented. -##### Channel Type "number" - -| Parameter | Description | Default DPT | -|-----------|---------------|-------------| -| ga | Group address | 9.001 | - -Note: Using the Units Of Measurement feature of openHAB (Quantitytype) requires that the DPT value is set correctly. -Automatic type conversion will be applied if required. - -##### Channel Type "string" - -| Parameter | Description | Default DPT | -|-----------|---------------|-------------| -| ga | Group address | 16.001 | - -##### Channel Type "datetime" +##### Channel Type `datetime`, `datetime-control` | Parameter | Description | Default DPT | |-----------|---------------|-------------| | ga | Group address | 19.001 | +##### Channel Type `dimmer`, `dimmer-control` + +| Parameter | Description | Default DPT | +|------------------|----------------------------------------|-------------| +| switch | Group address for the binary switch | 1.001 | +| position | Group address of the absolute position | 5.001 | +| increaseDecrease | Group address for relative movement | 3.007 | + +##### Channel Type `number`, `number-control` + +| Parameter | Description | Default DPT | +|-----------|---------------|-------------| +| ga | Group address | 9.001 | + +Note: The `number` channel has full support for Units Of Measurement (UoM). + +Using the UoM feature of openHAB (QuantityType) requires that the DPT value is set correctly. +Automatic type conversion will be applied if required. + +Incoming values from the KNX bus are converted to values with units (e.g. `23 °C`). +If the channel is linked to the correct item-type (`Number:Temperature` in this case) the display unit can be controlled by item metadata (e.g. `%.1f °F` for 1 digit of precision in Fahrenheit). +The unit is stripped if the channel is linked to a plain number item (type `Number`). + +Outgoing values with unit are first converted to the unit associated with the DPT (e.g. a value of `10 °F` is converted to `-8.33 °C` if the channel has DPT 9.001). +Values from plain number channels are sent as-is (without any conversion). + +##### Channel Type `rollershutter`, `rollershutter-control` + +| Parameter | Description | Default DPT | +|-----------|-----------------------------------------|-------------| +| upDown | Group address for relative movement | 1.008 | +| stopMove | Group address for stopping | 1.010 | +| position | Group address for the absolute position | 5.001 | + +##### Channel Type `string`, `string-control` + +| Parameter | Description | Default DPT | +|-----------|---------------|-------------| +| ga | Group address | 16.001 | + +##### Channel Type `switch`, `switch-control` + +| Parameter | Description | Default DPT | +|-----------|-------------------------------------|-------------| +| ga | Group address for the binary switch | 1.001 | + #### Control Channel Types In contrast to the standard channels above, the control channel types are used for cases where the KNX bus does not own the physical state of a device. This could for example be the case if a lamp from another binding should be controlled by a KNX wall switch. -If from the KNX bus a `GroupValueRead` telegram is sent to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus. +When a `GroupValueRead` telegram is sent from the KNX bus to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus. ##### Channel Type "switch-control" @@ -165,14 +194,6 @@ If from the KNX bus a `GroupValueRead` telegram is sent to a *-control Channel, | increaseDecrease | Group address for relative movement | 3.007 | | frequency | Increase/Decrease frequency in milliseconds in case the binding should handle that (0 if the KNX device sends the commands repeatedly itself) | 0 | -##### Channel Type "color-control" - -| Parameter | Description | Default DPT | -|------------------|----------------------------------------|-------------| -| hsb | Group address for color | 232.600 | -| switch | Group address for the binary switch | 1.001 | -| position | Group address of the absolute position | 5.001 | -| increaseDecrease | Group address for relative movement | 3.007 | ##### Channel Type "rollershutter-control" @@ -197,6 +218,8 @@ A change would break all existing installations and is therefore not implemented |-----------|---------------|-------------| | ga | Group address | 9.001 | +For UoM support see the explanations of the `number` channel. + ##### Channel Type "string-control" | Parameter | Description | Default DPT | @@ -340,14 +363,14 @@ Bridge knx:ip:bridge [ knx.items: ```java -Switch demoSwitch "Light [%s]" { channel="knx:device:bridge:generic:demoSwitch" } -Color demoColorLight "Color [%s]" { channel="knx:device:bridge:generic:demoColorLight" } -Dimmer demoDimmer "Dimmer [%d %%]" { channel="knx:device:bridge:generic:demoDimmer" } -Rollershutter demoRollershutter "Shade [%d %%]" { channel="knx:device:bridge:generic:demoRollershutter" } -Contact demoContact "Front Door [%s]" { channel="knx:device:bridge:generic:demoContact" } -Number demoTemperature "Temperature [%.1f °C]" { channel="knx:device:bridge:generic:demoTemperature" } -String demoString "Message of the day [%s]" { channel="knx:device:bridge:generic:demoString" } -DateTime demoDatetime "Alarm [%1$tH:%1$tM]" { channel="knx:device:bridge:generic:demoDatetime" } +Switch demoSwitch "Light [%s]" { channel="knx:device:bridge:generic:demoSwitch" } +Color demoColorLight "Color [%s]" { channel="knx:device:bridge:generic:demoColorLight" } +Dimmer demoDimmer "Dimmer [%d %%]" { channel="knx:device:bridge:generic:demoDimmer" } +Rollershutter demoRollershutter "Shade [%d %%]" { channel="knx:device:bridge:generic:demoRollershutter" } +Contact demoContact "Front Door [%s]" { channel="knx:device:bridge:generic:demoContact" } +Number:Temperature demoTemperature "Temperature [%.1f °C]" { channel="knx:device:bridge:generic:demoTemperature" } +String demoString "Message of the day [%s]" { channel="knx:device:bridge:generic:demoString" } +DateTime demoDatetime "Alarm [%1$tH:%1$tM]" { channel="knx:device:bridge:generic:demoDatetime" } ``` knx.sitemap: diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java index b2557e545f7..7e75da0f1b8 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java @@ -12,11 +12,7 @@ */ package org.openhab.binding.knx.internal; -import static java.util.stream.Collectors.toSet; - -import java.util.Collections; import java.util.Set; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -32,6 +28,10 @@ public class KNXBindingConstants { public static final String BINDING_ID = "knx"; + // Global config + public static final String CONFIG_DISABLE_UOM = "disableUoM"; + public static boolean disableUoM = false; + // Thing Type UIDs public static final ThingTypeUID THING_TYPE_IP_BRIDGE = new ThingTypeUID(BINDING_ID, "ip"); public static final ThingTypeUID THING_TYPE_SERIAL_BRIDGE = new ThingTypeUID(BINDING_ID, "serial"); @@ -84,7 +84,8 @@ public class KNXBindingConstants { public static final String CHANNEL_SWITCH = "switch"; public static final String CHANNEL_SWITCH_CONTROL = "switch-control"; - public static final Set CONTROL_CHANNEL_TYPES = Collections.unmodifiableSet(Stream.of(CHANNEL_COLOR_CONTROL, // + public static final Set CONTROL_CHANNEL_TYPES = Set.of( // + CHANNEL_COLOR_CONTROL, // CHANNEL_CONTACT_CONTROL, // CHANNEL_DATETIME_CONTROL, // CHANNEL_DIMMER_CONTROL, // @@ -92,7 +93,7 @@ public class KNXBindingConstants { CHANNEL_ROLLERSHUTTER_CONTROL, // CHANNEL_STRING_CONTROL, // CHANNEL_SWITCH_CONTROL // - ).collect(toSet())); + ); public static final String CHANNEL_RESET = "reset"; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java deleted file mode 100644 index 4a0892c00f8..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.channel; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -import tuwien.auto.calimero.GroupAddress; -import tuwien.auto.calimero.KNXFormatException; - -/** - * Base class for telegram meta-data - * - * @author Simon Kaufmann - initial contribution and API. - * - */ -@NonNullByDefault -public abstract class AbstractSpec { - - private String dpt; - - protected AbstractSpec(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) { - if (channelConfiguration != null) { - String configuredDPT = channelConfiguration.getDPT(); - this.dpt = configuredDPT != null ? configuredDPT : defaultDPT; - } else { - this.dpt = defaultDPT; - } - } - - /** - * Helper method to convert a {@link GroupAddressConfiguration} into a {@link GroupAddress}. - * - * @param ga the group address configuration - * @return a group address object - */ - protected final GroupAddress toGroupAddress(GroupAddressConfiguration ga) { - try { - return new GroupAddress(ga.getGA()); - } catch (KNXFormatException e) { - throw new IllegalArgumentException(e); - } - } - - /** - * Return the data point type. - *

- * See {@link org.openhab.binding.knx.internal.client.InboundSpec#getDPT()} and - * {@link org.openhab.binding.knx.internal.client.OutboundSpec#getDPT()}. - * - * @return the data point type. - */ - public final String getDPT() { - return dpt; - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ChannelConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ChannelConfiguration.java deleted file mode 100644 index 9d6545a6b2a..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ChannelConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.channel; - -import static java.util.stream.Collectors.toList; - -import java.util.List; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Data structure representing the content of a channel's group address configuration. - * - * @author Simon Kaufmann - initial contribution and API. - * - */ -@NonNullByDefault -public class ChannelConfiguration { - - private final @Nullable String dpt; - private final GroupAddressConfiguration mainGA; - private final List listenGAs; - - public ChannelConfiguration(@Nullable String dpt, GroupAddressConfiguration mainGA, - List listenGAs) { - this.dpt = dpt; - this.mainGA = mainGA; - this.listenGAs = listenGAs; - } - - public @Nullable String getDPT() { - return dpt; - } - - public GroupAddressConfiguration getMainGA() { - return mainGA; - } - - public List getListenGAs() { - return Stream.concat(Stream.of(mainGA), listenGAs.stream()).collect(toList()); - } - - public List getReadGAs() { - return getListenGAs().stream().filter(ga -> ga.isRead()).collect(toList()); - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/GroupAddressConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/GroupAddressConfiguration.java index b4d3643c4bb..a24ff68c540 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/GroupAddressConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/GroupAddressConfiguration.java @@ -12,41 +12,106 @@ */ package org.openhab.binding.knx.internal.channel; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.GroupAddress; +import tuwien.auto.calimero.KNXFormatException; /** - * Data structure representing a single group address configuration within a channel configuration parameter. + * Data structure representing the content of a channel's group address configuration. * * @author Simon Kaufmann - initial contribution and API. * */ @NonNullByDefault public class GroupAddressConfiguration { + public static final Logger LOGGER = LoggerFactory.getLogger(GroupAddressConfiguration.class); - private final String ga; - private final boolean read; + private static final Pattern PATTERN_GA_CONFIGURATION = Pattern.compile( + "^((?[1-9][0-9]{0,2}\\.[0-9]{3,5}):)?(?<)?(?[0-9]{1,5}(/[0-9]{1,4}){0,2})(?(\\+(<)?(?[0-9]{1,5}(/[0-9]{1,4}){0,2}))"); - public GroupAddressConfiguration(String ga, boolean read) { - super(); - this.ga = ga; - this.read = read; + private final @Nullable String dpt; + private final GroupAddress mainGA; + private final Set listenGAs; + private final Set readGAs; + + private GroupAddressConfiguration(@Nullable String dpt, GroupAddress mainGA, Set listenGAs, + Set readGAs) { + this.dpt = dpt; + this.mainGA = mainGA; + this.listenGAs = listenGAs; + this.readGAs = readGAs; } - /** - * The group address. - * - * @return the group address. - */ - public String getGA() { - return ga; + public @Nullable String getDPT() { + return dpt; } - /** - * Denotes whether the group address is marked to be actively read from. - * - * @return {@code true} if read requests should be issued to this address - */ - public boolean isRead() { - return read; + public GroupAddress getMainGA() { + return mainGA; + } + + public Set getListenGAs() { + return listenGAs; + } + + public Set getReadGAs() { + return readGAs; + } + + public static @Nullable GroupAddressConfiguration parse(@Nullable Object configuration) { + if (!(configuration instanceof String)) { + return null; + } + + Matcher matcher = PATTERN_GA_CONFIGURATION.matcher(((String) configuration).replace(" ", "")); + if (matcher.matches()) { + // Listen GAs + String input = matcher.group("listenGAs"); + Matcher m2 = PATTERN_LISTEN_GA.matcher(input); + Set listenGAs = new HashSet<>(); + Set readGAs = new HashSet<>(); + while (m2.find()) { + String ga = m2.group("GA"); + try { + GroupAddress groupAddress = new GroupAddress(ga); + listenGAs.add(groupAddress); + if (m2.group("read") != null) { + readGAs.add(groupAddress); + } + } catch (KNXFormatException e) { + LOGGER.warn("Failed to create GroupAddress from {}", ga); + return null; + } + } + + // Main GA + String mainGA = matcher.group("mainGA"); + try { + GroupAddress groupAddress = new GroupAddress(mainGA); + listenGAs.add(groupAddress); // also listening to main GA + if (matcher.group("read") != null) { + readGAs.add(groupAddress); // also reading main GA + } + return new GroupAddressConfiguration(matcher.group("dpt"), groupAddress, listenGAs, readGAs); + } catch (KNXFormatException e) { + LOGGER.warn("Failed to create GroupAddress from {}", mainGA); + return null; + } + } else { + LOGGER.warn("Failed parsing channel configuration '{}'.", configuration); + } + + return null; } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannel.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannel.java new file mode 100644 index 00000000000..01e407963a5 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannel.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.channel; + +import static java.util.stream.Collectors.*; +import static org.openhab.binding.knx.internal.KNXBindingConstants.CONTROL_CHANNEL_TYPES; +import static org.openhab.binding.knx.internal.KNXBindingConstants.GA; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.knx.internal.client.InboundSpec; +import org.openhab.binding.knx.internal.client.OutboundSpec; +import org.openhab.binding.knx.internal.dpt.DPTUtil; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.types.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.GroupAddress; + +/** + * Meta-data abstraction for the KNX channel configurations. + * + * @author Simon Kaufmann - initial contribution and API + * @author Jan N. Klug - refactored from type definition to channel instance + * + */ +@NonNullByDefault +public abstract class KNXChannel { + private final Logger logger = LoggerFactory.getLogger(KNXChannel.class); + private final Set gaKeys; + + private final Map groupAddressConfigurations = new HashMap<>(); + private final Set listenAddresses = new HashSet<>(); + private final Set writeAddresses = new HashSet<>(); + private final String channelType; + private final ChannelUID channelUID; + private final boolean isControl; + private final Class preferredType; + + KNXChannel(List> acceptedTypes, Channel channel) { + this(Set.of(GA), acceptedTypes, channel); + } + + KNXChannel(Set gaKeys, List> acceptedTypes, Channel channel) { + this.gaKeys = gaKeys; + this.preferredType = acceptedTypes.get(0); + + // this is safe because we already checked the presence of the ChannelTypeUID before + this.channelType = Objects.requireNonNull(channel.getChannelTypeUID()).getId(); + this.channelUID = channel.getUID(); + this.isControl = CONTROL_CHANNEL_TYPES.contains(channelType); + + // build map of ChannelConfigurations and GA lists + Configuration configuration = channel.getConfiguration(); + gaKeys.forEach(key -> { + GroupAddressConfiguration groupAddressConfiguration = GroupAddressConfiguration + .parse(configuration.get(key)); + if (groupAddressConfiguration != null) { + // check DPT configuration (if set) is compatible with item + String dpt = groupAddressConfiguration.getDPT(); + if (dpt != null) { + Set> types = DPTUtil.getAllowedTypes(dpt); + if (acceptedTypes.stream().noneMatch(types::contains)) { + logger.warn("Configured DPT '{}' is incompatible with accepted types '{}' for channel '{}'", + dpt, acceptedTypes, channelUID); + } + } + groupAddressConfigurations.put(key, groupAddressConfiguration); + // store address configuration for re-use + listenAddresses.addAll(groupAddressConfiguration.getListenGAs()); + writeAddresses.add(groupAddressConfiguration.getMainGA()); + } + }); + } + + public String getChannelType() { + return channelType; + } + + public ChannelUID getChannelUID() { + return channelUID; + } + + public boolean isControl() { + return isControl; + } + + public Class preferredType() { + return preferredType; + } + + public final Set getAllGroupAddresses() { + return listenAddresses; + } + + public final Set getWriteAddresses() { + return writeAddresses; + } + + public final @Nullable OutboundSpec getCommandSpec(Type command) { + logger.trace("getCommandSpec checking keys '{}' for command '{}' ({})", gaKeys, command, command.getClass()); + for (Map.Entry entry : groupAddressConfigurations.entrySet()) { + String dpt = Objects.requireNonNullElse(entry.getValue().getDPT(), getDefaultDPT(entry.getKey())); + Set> expectedTypeClass = DPTUtil.getAllowedTypes(dpt); + if (expectedTypeClass.contains(command.getClass())) { + logger.trace("getCommandSpec key '{}' has expectedTypeClass '{}', matching command '{}' and dpt '{}'", + entry.getKey(), expectedTypeClass, command, dpt); + return new WriteSpecImpl(entry.getValue(), dpt, command); + } + } + logger.trace("getCommandSpec no Spec found!"); + return null; + } + + public final List getReadSpec() { + return groupAddressConfigurations.entrySet().stream() + .map(entry -> new ReadRequestSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey()))) + .filter(spec -> !spec.getGroupAddresses().isEmpty()).collect(toList()); + } + + public final @Nullable InboundSpec getListenSpec(GroupAddress groupAddress) { + return groupAddressConfigurations.entrySet().stream() + .map(entry -> new ListenSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey()))) + .filter(spec -> spec.getGroupAddresses().contains(groupAddress)).findFirst().orElse(null); + } + + public final @Nullable OutboundSpec getResponseSpec(GroupAddress groupAddress, Type value) { + return groupAddressConfigurations.entrySet().stream() + .map(entry -> new ReadResponseSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey()), value)) + .filter(spec -> spec.matchesDestination(groupAddress)).findFirst().orElse(null); + } + + protected abstract String getDefaultDPT(String gaConfigKey); +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelFactory.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelFactory.java new file mode 100644 index 00000000000..826ed573d00 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelFactory.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.channel; + +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * Helper class to find the matching {@link KNXChannel} for any given {@link ChannelTypeUID}. + * + * @author Simon Kaufmann - Initial contribution + * @author Jan N. Klug - Refactored to factory class + * + */ +@NonNullByDefault +public final class KNXChannelFactory { + + private static final Map, Function> TYPES = Map.ofEntries( // + Map.entry(TypeColor.SUPPORTED_CHANNEL_TYPES, TypeColor::new), // + Map.entry(TypeContact.SUPPORTED_CHANNEL_TYPES, TypeContact::new), // + Map.entry(TypeDateTime.SUPPORTED_CHANNEL_TYPES, TypeDateTime::new), // + Map.entry(TypeDimmer.SUPPORTED_CHANNEL_TYPES, TypeDimmer::new), // + Map.entry(TypeNumber.SUPPORTED_CHANNEL_TYPES, TypeNumber::new), // + Map.entry(TypeRollershutter.SUPPORTED_CHANNEL_TYPES, TypeRollershutter::new), // + Map.entry(TypeString.SUPPORTED_CHANNEL_TYPES, TypeString::new), // + Map.entry(TypeSwitch.SUPPORTED_CHANNEL_TYPES, TypeSwitch::new)); + + private KNXChannelFactory() { + // prevent instantiation + } + + public static KNXChannel createKnxChannel(Channel channel) throws IllegalArgumentException { + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null) { + throw new IllegalArgumentException("Could not determine ChannelTypeUID for channel " + channel.getUID()); + } + + String channelType = channelTypeUID.getId(); + + Function supplier = TYPES.entrySet().stream().filter(e -> e.getKey().contains(channelType)) + .map(Map.Entry::getValue).findFirst() + .orElseThrow(() -> new IllegalArgumentException(channelTypeUID + " is not a valid channel type ID")); + + return supplier.apply(channel); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelType.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelType.java deleted file mode 100644 index 170499fa8db..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelType.java +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.channel; - -import static java.util.stream.Collectors.*; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.knx.internal.KNXTypeMapper; -import org.openhab.binding.knx.internal.client.InboundSpec; -import org.openhab.binding.knx.internal.client.OutboundSpec; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.types.Type; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import tuwien.auto.calimero.GroupAddress; -import tuwien.auto.calimero.KNXFormatException; - -/** - * Meta-data abstraction for the KNX channel configurations. - * - * @author Simon Kaufmann - initial contribution and API. - * - */ -@NonNullByDefault -public abstract class KNXChannelType { - - private static final Pattern PATTERN = Pattern.compile( - "^((?[0-9]{1,3}\\.[0-9]{3,4}):)?(?\\<)?(?[0-9]{1,5}(/[0-9]{1,4}){0,2})(?(\\+(\\\\<)?(?[0-9]{1,5}(/[0-9]{1,4}){0,2}))"); - - private final Logger logger = LoggerFactory.getLogger(KNXChannelType.class); - private final Set channelTypeIDs; - - KNXChannelType(String... channelTypeIDs) { - this.channelTypeIDs = new HashSet<>(Arrays.asList(channelTypeIDs)); - } - - final Set getChannelIDs() { - return channelTypeIDs; - } - - @Nullable - protected final ChannelConfiguration parse(@Nullable String fancy) { - if (fancy == null) { - return null; - } - Matcher matcher = PATTERN.matcher(fancy.replace(" ", "")); - - if (matcher.matches()) { - // Listen GAs - String input = matcher.group("listenGAs"); - Matcher m2 = PATTERN_LISTEN.matcher(input); - List listenGAs = new LinkedList<>(); - while (m2.find()) { - listenGAs.add(new GroupAddressConfiguration(m2.group("GA"), m2.group("read") != null)); - } - - // Main GA - GroupAddressConfiguration mainGA = new GroupAddressConfiguration(matcher.group("mainGA"), - matcher.group("read") != null); - - return new ChannelConfiguration(matcher.group("dpt"), mainGA, listenGAs); - } - return null; - } - - protected abstract Set getAllGAKeys(); - - public final Set getListenAddresses(Configuration channelConfiguration) { - Set ret = new HashSet<>(); - for (String key : getAllGAKeys()) { - ChannelConfiguration conf = parse((String) channelConfiguration.get(key)); - if (conf != null) { - ret.addAll(conf.getListenGAs().stream().map(this::toGroupAddress).collect(toSet())); - } - } - return ret; - } - - public final Set getReadAddresses(Configuration channelConfiguration) { - Set ret = new HashSet<>(); - for (String key : getAllGAKeys()) { - ChannelConfiguration conf = parse((String) channelConfiguration.get(key)); - if (conf != null) { - ret.addAll(conf.getReadGAs().stream().map(this::toGroupAddress).collect(toSet())); - } - } - return ret; - } - - public final Set getWriteAddresses(Configuration channelConfiguration) { - Set ret = new HashSet<>(); - for (String key : getAllGAKeys()) { - ChannelConfiguration conf = parse((String) channelConfiguration.get(key)); - if (conf != null) { - GroupAddress ga = toGroupAddress(conf.getMainGA()); - if (ga != null) { - ret.add(ga); - } - } - } - return ret; - } - - private @Nullable GroupAddress toGroupAddress(GroupAddressConfiguration ga) { - try { - return new GroupAddress(ga.getGA()); - } catch (KNXFormatException e) { - logger.warn("Could not parse group address '{}'", ga.getGA()); - } - return null; - } - - protected final Set getAddresses(@Nullable Configuration configuration, Iterable addresses) - throws KNXFormatException { - Set ret = new HashSet<>(); - for (String address : addresses) { - if (configuration != null && configuration.get(address) != null) { - ret.add(new GroupAddress((String) configuration.get(address))); - } - } - return ret; - } - - protected final boolean isEquals(@Nullable Configuration configuration, String address, GroupAddress groupAddress) - throws KNXFormatException { - if (configuration != null && configuration.get(address) != null) { - return Objects.equals(new GroupAddress((String) configuration.get(address)), groupAddress); - } - return false; - } - - protected final Set asSet(String... values) { - return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(values))); - } - - public final @Nullable OutboundSpec getCommandSpec(Configuration configuration, KNXTypeMapper typeHelper, - Type command) throws KNXFormatException { - logger.trace("getCommandSpec testing Keys '{}' for command '{}'", getAllGAKeys(), command); - for (String key : getAllGAKeys()) { - ChannelConfiguration config = parse((String) configuration.get(key)); - if (config != null) { - String dpt = config.getDPT(); - if (dpt == null) { - dpt = getDefaultDPT(key); - } - Class expectedTypeClass = typeHelper.toTypeClass(dpt); - if (expectedTypeClass != null) { - if (expectedTypeClass.isInstance(command) - || ((expectedTypeClass == DecimalType.class) && (command instanceof QuantityType))) { - logger.trace( - "getCommandSpec key '{}' uses expectedTypeClass '{}' which isInstance for command '{}' and dpt '{}'", - key, expectedTypeClass, command, dpt); - return new WriteSpecImpl(config, dpt, command); - } - } - } - } - logger.trace("getCommandSpec no Spec found!"); - return null; - } - - public final List getReadSpec(Configuration configuration) throws KNXFormatException { - return getAllGAKeys().stream() - .map(key -> new ReadRequestSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key))) - .filter(spec -> !spec.getGroupAddresses().isEmpty()).collect(toList()); - } - - public final @Nullable InboundSpec getListenSpec(Configuration configuration, GroupAddress groupAddress) { - Optional result = getAllGAKeys().stream() - .map(key -> new ListenSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key))) - .filter(spec -> !spec.getGroupAddresses().isEmpty()) - .filter(spec -> spec.getGroupAddresses().contains(groupAddress)).findFirst(); - return result.isPresent() ? result.get() : null; - } - - protected abstract String getDefaultDPT(String gaConfigKey); - - public final @Nullable OutboundSpec getResponseSpec(Configuration configuration, GroupAddress groupAddress, - Type type) throws KNXFormatException { - Optional result = getAllGAKeys().stream() - .map(key -> new ReadResponseSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key), type)) - .filter(spec -> groupAddress.equals(spec.getGroupAddress())).findFirst(); - return result.isPresent() ? result.get() : null; - } - - @Override - public String toString() { - return channelTypeIDs.toString(); - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelTypes.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelTypes.java deleted file mode 100644 index 145b1a350b3..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelTypes.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.channel; - -import static java.util.stream.Collectors.toSet; - -import java.util.Collections; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.thing.type.ChannelTypeUID; - -/** - * Helper class to find the matching {@link KNXChannelType} for any given {@link ChannelTypeUID}. - * - * @author Simon Kaufmann - initial contribution and API. - * - */ -@NonNullByDefault -public final class KNXChannelTypes { - - private static final Set TYPES = Collections.unmodifiableSet(Stream.of(// - new TypeColor(), // - new TypeContact(), // - new TypeDateTime(), // - new TypeDimmer(), // - new TypeNumber(), // - new TypeRollershutter(), // - new TypeString(), // - new TypeSwitch() // - ).collect(toSet())); - - private KNXChannelTypes() { - // prevent instantiation - } - - public static KNXChannelType getType(@Nullable ChannelTypeUID channelTypeUID) throws IllegalArgumentException { - Objects.requireNonNull(channelTypeUID); - for (KNXChannelType c : TYPES) { - if (c.getChannelIDs().contains(channelTypeUID.getId())) { - return c; - } - } - throw new IllegalArgumentException(channelTypeUID.getId() + " is not a valid value channel type ID"); - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java index e56ca5d3973..cd286dbf418 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java @@ -12,13 +12,10 @@ */ package org.openhab.binding.knx.internal.channel; -import static java.util.stream.Collectors.toList; - -import java.util.Collections; -import java.util.List; +import java.util.Objects; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.InboundSpec; import tuwien.auto.calimero.GroupAddress; @@ -30,21 +27,22 @@ import tuwien.auto.calimero.GroupAddress; * */ @NonNullByDefault -public class ListenSpecImpl extends AbstractSpec implements InboundSpec { +public class ListenSpecImpl implements InboundSpec { + private final String dpt; + private final Set listenAddresses; - private final List listenAddresses; - - public ListenSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) { - super(channelConfiguration, defaultDPT); - if (channelConfiguration != null) { - this.listenAddresses = channelConfiguration.getListenGAs().stream().map(this::toGroupAddress) - .collect(toList()); - } else { - this.listenAddresses = Collections.emptyList(); - } + public ListenSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) { + this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT); + this.listenAddresses = groupAddressConfiguration.getListenGAs(); } - public List getGroupAddresses() { + @Override + public String getDPT() { + return dpt; + } + + @Override + public Set getGroupAddresses() { return listenAddresses; } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadRequestSpecImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadRequestSpecImpl.java index e3f8d31abc8..6e3173b7906 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadRequestSpecImpl.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadRequestSpecImpl.java @@ -12,13 +12,10 @@ */ package org.openhab.binding.knx.internal.channel; -import static java.util.stream.Collectors.toList; - -import java.util.Collections; -import java.util.List; +import java.util.Objects; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.InboundSpec; import tuwien.auto.calimero.GroupAddress; @@ -30,21 +27,22 @@ import tuwien.auto.calimero.GroupAddress; * */ @NonNullByDefault -public class ReadRequestSpecImpl extends AbstractSpec implements InboundSpec { +public class ReadRequestSpecImpl implements InboundSpec { + private final String dpt; + private final Set readAddresses; - private final List readAddresses; - - public ReadRequestSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) { - super(channelConfiguration, defaultDPT); - if (channelConfiguration != null) { - this.readAddresses = channelConfiguration.getReadGAs().stream().map(this::toGroupAddress).collect(toList()); - } else { - this.readAddresses = Collections.emptyList(); - } + public ReadRequestSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) { + this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT); + this.readAddresses = groupAddressConfiguration.getReadGAs(); } @Override - public List getGroupAddresses() { + public String getDPT() { + return dpt; + } + + @Override + public Set getGroupAddresses() { return readAddresses; } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadResponseSpecImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadResponseSpecImpl.java index 5a7ecbbd6bd..6c17aa87518 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadResponseSpecImpl.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadResponseSpecImpl.java @@ -12,8 +12,9 @@ */ package org.openhab.binding.knx.internal.channel; +import java.util.Objects; + import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.OutboundSpec; import org.openhab.core.types.Type; @@ -26,28 +27,34 @@ import tuwien.auto.calimero.GroupAddress; * */ @NonNullByDefault -public class ReadResponseSpecImpl extends AbstractSpec implements OutboundSpec { +public class ReadResponseSpecImpl implements OutboundSpec { + private final String dpt; + private final GroupAddress groupAddress; + private final Type value; - private final @Nullable GroupAddress groupAddress; - private final Type type; - - public ReadResponseSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT, Type state) { - super(channelConfiguration, defaultDPT); - if (channelConfiguration != null) { - this.groupAddress = toGroupAddress(channelConfiguration.getMainGA()); - } else { - this.groupAddress = null; - } - this.type = state; + public ReadResponseSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type state) { + this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT); + this.groupAddress = groupAddressConfiguration.getMainGA(); + this.value = state; } @Override - public @Nullable GroupAddress getGroupAddress() { + public String getDPT() { + return dpt; + } + + @Override + public GroupAddress getGroupAddress() { return groupAddress; } @Override - public Type getType() { - return type; + public Type getValue() { + return value; + } + + @Override + public boolean matchesDestination(GroupAddress groupAddress) { + return groupAddress.equals(this.groupAddress); } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java index e79fb4e36fd..527af791147 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java @@ -12,13 +12,17 @@ */ package org.openhab.binding.knx.internal.channel; -import static java.util.stream.Collectors.toSet; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; +import java.util.List; import java.util.Set; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; +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.Channel; import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; @@ -32,15 +36,12 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorRGB; * */ @NonNullByDefault -class TypeColor extends KNXChannelType { +class TypeColor extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_COLOR, CHANNEL_COLOR_CONTROL); - TypeColor() { - super(CHANNEL_COLOR, CHANNEL_COLOR_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA).collect(toSet()); + TypeColor(Channel channel) { + super(Set.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA), + List.of(HSBType.class, PercentType.class, OnOffType.class, IncreaseDecreaseType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeContact.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeContact.java index 363b4769904..c53db439ad8 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeContact.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeContact.java @@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.Channel; import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; @@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; * */ @NonNullByDefault -class TypeContact extends KNXChannelType { +class TypeContact extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL); - TypeContact() { - super(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Collections.singleton(GA); + TypeContact(Channel channel) { + super(List.of(OpenClosedType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDateTime.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDateTime.java index b2e36274b44..d9a3d9ccea1 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDateTime.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDateTime.java @@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.thing.Channel; import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime; @@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime; * */ @NonNullByDefault -class TypeDateTime extends KNXChannelType { +class TypeDateTime extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL); - TypeDateTime() { - super(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Collections.singleton(GA); + TypeDateTime(Channel channel) { + super(List.of(DateTimeType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java index 5e01f0386ba..94006c4c489 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java @@ -14,10 +14,15 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; +import java.util.List; import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +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.Channel; import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; @@ -30,15 +35,12 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; * */ @NonNullByDefault -class TypeDimmer extends KNXChannelType { +class TypeDimmer extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL); - TypeDimmer() { - super(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Set.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA); + TypeDimmer(Channel channel) { + super(Set.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA), + List.of(PercentType.class, OnOffType.class, IncreaseDecreaseType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeNumber.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeNumber.java index f5ea76fe754..8caf885fd8e 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeNumber.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeNumber.java @@ -14,10 +14,13 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Channel; /** * number channel type description @@ -26,19 +29,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; * */ @NonNullByDefault -class TypeNumber extends KNXChannelType { +class TypeNumber extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_NUMBER, CHANNEL_NUMBER_CONTROL); - TypeNumber() { - super(CHANNEL_NUMBER, CHANNEL_NUMBER_CONTROL); + TypeNumber(Channel channel) { + super(List.of(DecimalType.class, QuantityType.class), channel); } @Override protected String getDefaultDPT(String gaConfigKey) { return "9.001"; } - - @Override - protected Set getAllGAKeys() { - return Collections.singleton(GA); - } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java index 94a41563d01..dfe037bd4ba 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java @@ -14,10 +14,15 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; +import java.util.List; import java.util.Objects; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.Channel; import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; @@ -29,10 +34,13 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; * */ @NonNullByDefault -class TypeRollershutter extends KNXChannelType { +class TypeRollershutter extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_ROLLERSHUTTER, + CHANNEL_ROLLERSHUTTER_CONTROL); - TypeRollershutter() { - super(CHANNEL_ROLLERSHUTTER, CHANNEL_ROLLERSHUTTER_CONTROL); + TypeRollershutter(Channel channel) { + super(Set.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA), + List.of(PercentType.class, UpDownType.class, StopMoveType.class), channel); } @Override @@ -48,9 +56,4 @@ class TypeRollershutter extends KNXChannelType { } throw new IllegalArgumentException("GA configuration '" + gaConfigKey + "' is not supported"); } - - @Override - protected Set getAllGAKeys() { - return Set.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA); - } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeString.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeString.java index 4c755b126fd..8a093712be7 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeString.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeString.java @@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; import tuwien.auto.calimero.dptxlator.DPTXlatorString; @@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorString; * */ @NonNullByDefault -class TypeString extends KNXChannelType { +class TypeString extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_STRING, CHANNEL_STRING_CONTROL); - TypeString() { - super(CHANNEL_STRING, CHANNEL_STRING_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Collections.singleton(GA); + TypeString(Channel channel) { + super(List.of(StringType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeSwitch.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeSwitch.java index cbf415328fb..b3a2fb2d821 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeSwitch.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeSwitch.java @@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; -import java.util.Collections; +import java.util.List; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Channel; import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; @@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; * */ @NonNullByDefault -class TypeSwitch extends KNXChannelType { +class TypeSwitch extends KNXChannel { + public static final Set SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL); - TypeSwitch() { - super(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL); - } - - @Override - protected Set getAllGAKeys() { - return Collections.singleton(GA); + TypeSwitch(Channel channel) { + super(List.of(OnOffType.class), channel); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/WriteSpecImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/WriteSpecImpl.java index ef5bb50917c..3e3b314a3a9 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/WriteSpecImpl.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/WriteSpecImpl.java @@ -12,13 +12,13 @@ */ package org.openhab.binding.knx.internal.channel; +import java.util.Objects; + import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.OutboundSpec; import org.openhab.core.types.Type; import tuwien.auto.calimero.GroupAddress; -import tuwien.auto.calimero.KNXFormatException; /** * Command meta-data @@ -27,29 +27,34 @@ import tuwien.auto.calimero.KNXFormatException; * */ @NonNullByDefault -public class WriteSpecImpl extends AbstractSpec implements OutboundSpec { +public class WriteSpecImpl implements OutboundSpec { + private final String dpt; + private final Type value; + private final GroupAddress groupAddress; - private final Type type; - private final @Nullable GroupAddress groupAddress; - - public WriteSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT, Type type) - throws KNXFormatException { - super(channelConfiguration, defaultDPT); - if (channelConfiguration != null) { - this.groupAddress = new GroupAddress(channelConfiguration.getMainGA().getGA()); - } else { - this.groupAddress = null; - } - this.type = type; + public WriteSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type value) { + this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT); + this.groupAddress = groupAddressConfiguration.getMainGA(); + this.value = value; } @Override - public Type getType() { - return type; + public String getDPT() { + return dpt; } @Override - public @Nullable GroupAddress getGroupAddress() { + public Type getValue() { + return value; + } + + @Override + public GroupAddress getGroupAddress() { return groupAddress; } + + @Override + public boolean matchesDestination(GroupAddress groupAddress) { + return groupAddress.equals(this.groupAddress); + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java index 2ef00e9aa71..df3d42e34a5 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.knx.internal.client; +import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT; + import java.time.Duration; import java.util.Optional; import java.util.Set; @@ -25,8 +27,7 @@ import java.util.function.Consumer; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.knx.internal.KNXTypeMapper; -import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper; +import org.openhab.binding.knx.internal.dpt.ValueEncoder; import org.openhab.binding.knx.internal.handler.GroupAddressListener; import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; import org.openhab.core.thing.ThingStatus; @@ -82,7 +83,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private static final int MAX_SEND_ATTEMPTS = 2; private final Logger logger = LoggerFactory.getLogger(AbstractKNXClient.class); - private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper(); private final ThingUID thingUID; private final int responseTimeout; @@ -119,23 +119,20 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien @Override public void groupWrite(ProcessEvent e) { - processEvent("Group Write", e, (listener, source, destination, asdu) -> { - listener.onGroupWrite(AbstractKNXClient.this, source, destination, asdu); - }); + processEvent("Group Write", e, (listener, source, destination, asdu) -> listener + .onGroupWrite(AbstractKNXClient.this, source, destination, asdu)); } @Override public void groupReadRequest(ProcessEvent e) { - processEvent("Group Read Request", e, (listener, source, destination, asdu) -> { - listener.onGroupRead(AbstractKNXClient.this, source, destination, asdu); - }); + processEvent("Group Read Request", e, (listener, source, destination, asdu) -> listener + .onGroupRead(AbstractKNXClient.this, source, destination, asdu)); } @Override public void groupReadResponse(ProcessEvent e) { - processEvent("Group Read Response", e, (listener, source, destination, asdu) -> { - listener.onGroupReadResponse(AbstractKNXClient.this, source, destination, asdu); - }); + processEvent("Group Read Response", e, (listener, source, destination, asdu) -> listener + .onGroupReadResponse(AbstractKNXClient.this, source, destination, asdu)); } }; @@ -151,21 +148,16 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien } public void initialize() { - if (!scheduleReconnectJob()) { - connect(); - } + connect(); } - private boolean scheduleReconnectJob() { + private void scheduleReconnectJob() { if (autoReconnectPeriod > 0) { // schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod; final String prefix = (state == ClientState.INIT) ? "re" : ""; logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS); connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS); - return true; - } else { - return false; } } @@ -181,7 +173,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private synchronized boolean connectIfNotAutomatic() { if (!isConnected()) { - return connectJob != null ? false : connect(); + return connectJob == null && connect(); } return true; } @@ -241,15 +233,14 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien // ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero). // Note for KNX Secure: SAL to be provided - ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, + this.responseCommunicator = new ProcessCommunicationResponder(link, new SecureApplicationLayer(link, Security.defaultInstallation())); - this.responseCommunicator = responseCommunicator; // register this class, callbacks will be triggered link.addLinkListener(this); // create a job carrying out read requests - busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause, + busJob = knxScheduler.scheduleWithFixedDelay(this::readNextQueuedDatapoint, 0, readingPause, TimeUnit.MILLISECONDS); statusUpdateCallback.updateStatus(ThingStatus.ONLINE); @@ -314,9 +305,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien pc.detach(); }); deviceInfoClient = null; - managementClient = nullify(managementClient, mc -> mc.detach()); - managementProcedures = nullify(managementProcedures, mp -> mp.detach()); - link = nullify(link, l -> l.close()); + managementClient = nullify(managementClient, ManagementClient::detach); + managementProcedures = nullify(managementProcedures, ManagementProcedures::detach); + link = nullify(link, KNXNetworkLink::close); logger.trace("Bridge {} disconnected from KNX bus", thingUID); } @@ -339,18 +330,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien } } - /** - * Transforms a {@link Type} into a datapoint type value for the KNX bus. - * - * @param type the {@link Type} to transform - * @param dpt the datapoint type to which should be converted - * @return the corresponding KNX datapoint type value as a string - */ - @Nullable - private String toDPTValue(Type type, String dpt) { - return typeHelper.toDPTValue(type, dpt); - } - // datapoint is null at end of the list, warning is misleading @SuppressWarnings("null") private void readNextQueuedDatapoint() { @@ -380,7 +359,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien } } catch (InterruptedException | CancellationException e) { logger.debug("Interrupted sending KNX read request"); - return; } catch (Exception e) { // Any other exception: Fail gracefully, i.e. notify user and continue reading next DP. // Not catching this would end the scheduled read for all DPs in case of an error. @@ -469,13 +447,13 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien } @Override - public final boolean registerGroupAddressListener(GroupAddressListener listener) { - return groupAddressListeners.add(listener); + public final void registerGroupAddressListener(GroupAddressListener listener) { + groupAddressListeners.add(listener); } @Override - public final boolean unregisterGroupAddressListener(GroupAddressListener listener) { - return groupAddressListeners.remove(listener); + public final void unregisterGroupAddressListener(GroupAddressListener listener) { + groupAddressListeners.remove(listener); } @Override @@ -499,7 +477,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien ProcessCommunicator processCommunicator = this.processCommunicator; KNXNetworkLink link = this.link; if (processCommunicator == null || link == null) { - logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})", + logger.debug("Cannot write to KNX bus (processCommunicator: {}, link: {})", processCommunicator == null ? "Not OK" : "OK", link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed")); return; @@ -508,9 +486,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien logger.trace("writeToKNX groupAddress '{}', commandSpec '{}'", groupAddress, commandSpec); - if (groupAddress != null) { - sendToKNX(processCommunicator, link, groupAddress, commandSpec.getDPT(), commandSpec.getType()); - } + sendToKNX(processCommunicator, groupAddress, commandSpec.getDPT(), commandSpec.getValue()); } @Override @@ -527,27 +503,26 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien logger.trace("respondToKNX groupAddress '{}', responseSpec '{}'", groupAddress, responseSpec); - if (groupAddress != null) { - sendToKNX(responseCommunicator, link, groupAddress, responseSpec.getDPT(), responseSpec.getType()); - } + sendToKNX(responseCommunicator, groupAddress, responseSpec.getDPT(), responseSpec.getValue()); } - private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, GroupAddress groupAddress, - String dpt, Type type) throws KNXException { + private void sendToKNX(ProcessCommunication communicator, GroupAddress groupAddress, String dpt, Type type) + throws KNXException { if (!connectIfNotAutomatic()) { return; } - Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0, dpt); - String mappedValue = toDPTValue(type, dpt); - - logger.trace("sendToKNX mappedValue: '{}' groupAddress: '{}'", mappedValue, groupAddress); - + Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0, + NORMALIZED_DPT.getOrDefault(dpt, dpt)); + String mappedValue = ValueEncoder.encode(type, dpt); if (mappedValue == null) { - logger.debug("Value '{}' cannot be mapped to datapoint '{}'", type, datapoint); + logger.debug("Value '{}' of type '{}' cannot be mapped to datapoint '{}'", type, type.getClass(), + datapoint); return; } - for (int i = 0; i < MAX_SEND_ATTEMPTS; i++) { + logger.trace("sendToKNX mappedValue: '{}' groupAddress: '{}'", mappedValue, groupAddress); + + for (int i = 0;; i++) { try { communicator.write(datapoint, mappedValue); logger.debug("Wrote value '{}' to datapoint '{}' ({}. attempt).", type, datapoint, i); diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/BusMessageListener.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/BusMessageListener.java index 37093dba916..73026f85c38 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/BusMessageListener.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/BusMessageListener.java @@ -33,7 +33,7 @@ public interface BusMessageListener { * @param destination * @param asdu */ - public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu); + void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu); /** * Called when the KNX bridge receives a group read telegram @@ -43,7 +43,7 @@ public interface BusMessageListener { * @param destination * @param asdu */ - public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu); + void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu); /** * Called when the KNX bridge receives a group read response telegram @@ -53,6 +53,5 @@ public interface BusMessageListener { * @param destination * @param asdu */ - public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, - byte[] asdu); + void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu); } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/InboundSpec.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/InboundSpec.java index ffa3deaaa9a..e5145b01804 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/InboundSpec.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/InboundSpec.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.knx.internal.client; -import java.util.List; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -39,5 +39,5 @@ public interface InboundSpec { * * @return a list of group addresses. */ - List getGroupAddresses(); + Set getGroupAddresses(); } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/KNXClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/KNXClient.java index cbbc98b0138..c54991c902a 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/KNXClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/KNXClient.java @@ -64,17 +64,15 @@ public interface KNXClient { * Register the given listener to be informed on KNX bus traffic. * * @param listener the listener - * @return {@code true} if it wasn't registered before */ - boolean registerGroupAddressListener(GroupAddressListener listener); + void registerGroupAddressListener(GroupAddressListener listener); /** * Remove the given listener. * * @param listener the listener - * @return {@code true} if it was successfully removed */ - boolean unregisterGroupAddressListener(GroupAddressListener listener); + void unregisterGroupAddressListener(GroupAddressListener listener); /** * Schedule the given data point for asynchronous reading. diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/NoOpClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/NoOpClient.java index 9bcc7382ce8..92d1f3131e9 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/NoOpClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/NoOpClient.java @@ -49,13 +49,11 @@ public class NoOpClient implements KNXClient { } @Override - public boolean registerGroupAddressListener(GroupAddressListener listener) { - return false; + public void registerGroupAddressListener(GroupAddressListener listener) { } @Override - public boolean unregisterGroupAddressListener(GroupAddressListener listener) { - return false; + public void unregisterGroupAddressListener(GroupAddressListener listener) { } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/OutboundSpec.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/OutboundSpec.java index f2951a1eec9..e8e349c3183 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/OutboundSpec.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/OutboundSpec.java @@ -13,7 +13,6 @@ package org.openhab.binding.knx.internal.client; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.Type; import tuwien.auto.calimero.GroupAddress; @@ -39,7 +38,6 @@ public interface OutboundSpec { * * @return the group address */ - @Nullable GroupAddress getGroupAddress(); /** @@ -47,5 +45,13 @@ public interface OutboundSpec { * * @return the command/state */ - Type getType(); + Type getValue(); + + /** + * Check if group address to be used matches a given group address. + * + * @param groupAddress group address to be compared + * @return true if addresses match + */ + boolean matchesDestination(GroupAddress groupAddress); } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/StatusUpdateCallback.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/StatusUpdateCallback.java index b4544216750..80b506b1fb0 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/StatusUpdateCallback.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/StatusUpdateCallback.java @@ -19,23 +19,29 @@ import org.openhab.core.thing.ThingStatusDetail; /** * Callback interface which enables the KNXClient implementations to update the thing status. * - * @author Simon Kaufmann - initial contribution and API. + * @author Simon Kaufmann - Initial contribution * */ @NonNullByDefault public interface StatusUpdateCallback { /** - * see BaseThingHandler + * Updates the status of the thing. * - * @param status + * see {@link org.openhab.core.thing.binding.BaseThingHandler} + * + * @param status the status */ void updateStatus(ThingStatus status); /** - * see BaseThingHandler + * Updates the status of the thing. * - * @param status + * see {@link org.openhab.core.thing.binding.BaseThingHandler} + * + * @param status the status + * @param statusDetail the detail of the status + * @param description the description of the status */ - void updateStatus(ThingStatus status, ThingStatusDetail thingStatusDetail, String message); + void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description); } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java index ece9352815b..eb144adea11 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** * {@link org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler} configuration * - * @author Simon Kaufmann - initial contribution and API + * @author Simon Kaufmann - Initial contribution * */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/DeviceConfig.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/DeviceConfig.java index 5746b6a86d5..207c2bce0c5 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/DeviceConfig.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/DeviceConfig.java @@ -22,7 +22,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class DeviceConfig { - private String address = ""; private boolean fetch = false; private int pingInterval = 0; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUnits.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUnits.java new file mode 100644 index 00000000000..21ae841118f --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUnits.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.dpt; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; + +import tuwien.auto.calimero.dptxlator.DPT; +import tuwien.auto.calimero.dptxlator.DPTXlator; +import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat; +import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; +import tuwien.auto.calimero.dptxlator.DptXlator2ByteSigned; + +/** + * This class provides the units for values depending on the DPT (if available) + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DPTUnits { + private static final Map DPT_UNIT_MAP = new HashMap<>(); + + private DPTUnits() { + // prevent instantiation + } + + /** + * get unit string for a given DPT + * + * @param dptId the KNX DPT + * @return unit string + */ + public static @Nullable String getUnitForDpt(String dptId) { + return DPT_UNIT_MAP.get(dptId); + } + + /** + * for testing purposes only + * + * @return stream of all unit strings + */ + static Stream getAllUnitStrings() { + return DPT_UNIT_MAP.values().stream(); + } + + static { + // try to get units from Calimeros "unit" field in DPTXlators + List> translators = List.of(DPTXlator2ByteUnsigned.class, DptXlator2ByteSigned.class, + DPTXlator2ByteFloat.class, DPTXlator4ByteUnsigned.class, DPTXlator4ByteSigned.class, + DPTXlator4ByteFloat.class, DPTXlator64BitSigned.class); + + for (Class translator : translators) { + Field[] fields = translator.getFields(); + for (Field field : fields) { + try { + Object o = field.get(null); + if (o instanceof DPT) { + DPT dpt = (DPT) o; + String unit = dpt.getUnit().replaceAll(" ", ""); + // Calimero provides some units (like "ms⁻²") that can't be parsed by our library because of the + // negative exponent + // replace with / + int index = unit.indexOf("⁻"); + if (index != -1) { + unit = unit.substring(0, index - 1) + "/" + unit.substring(index - 1).replace("⁻", ""); + } + if (!unit.isEmpty()) { + DPT_UNIT_MAP.put(dpt.getID(), unit); + } + } + } catch (IllegalAccessException e) { + // ignore errors + } + } + } + + // override/fix units where Calimero data is unparsable or missing + + // 8 bit unsigned (DPT 5) + DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_SCALING.getID(), Units.PERCENT.getSymbol()); // required to ensure + // correct conversion + DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_ANGLE.getID(), "°"); // Calimero returns Unicode + DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_PERCENT_U8.getID(), Units.PERCENT.getSymbol()); // required to ensure + // correct conversion + + // 8bit signed (DPT 6) + DPT_UNIT_MAP.put(DPTXlator8BitSigned.DPT_PERCENT_V8.getID(), Units.PERCENT.getSymbol()); // required to ensure + // correct conversion + + // two byte unsigned (DPT 7) + DPT_UNIT_MAP.remove(DPTXlator2ByteUnsigned.DPT_VALUE_2_UCOUNT.getID()); // counts have no unit + DPT_UNIT_MAP.put(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_10.getID(), "ms"); // according to spec, it is ms + DPT_UNIT_MAP.put(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_100.getID(), "ms"); // according to spec, it is ms + + // two byte signed (DPT 8) + DPT_UNIT_MAP.remove(DptXlator2ByteSigned.DptValueCount.getID()); // pulses habe no unit + + // 4 byte unsigned (DPT 12) + DPT_UNIT_MAP.remove(DPTXlator4ByteUnsigned.DPT_VALUE_4_UCOUNT.getID()); // counts have no unit + + // 4 byte signed (DPT 13) + DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY.getID(), Units.VAR_HOUR.toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY_KVARH.getID(), Units.KILOVAR_HOUR.toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_APPARENT_ENERGY_KVAH.getID(), + Units.KILOVOLT_AMPERE.multiply(Units.HOUR).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_FLOWRATE.getID(), Units.CUBICMETRE_PER_HOUR.toString()); + DPT_UNIT_MAP.remove(DPTXlator4ByteSigned.DPT_COUNT.getID()); // counts have no unit + + // four byte float (DPT 14) + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_CONDUCTANCE.getID(), Units.SIEMENS.toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ANGULAR_MOMENTUM.getID(), + Units.JOULE.multiply(Units.SECOND).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ACTIVITY.getID(), Units.BECQUEREL.toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRICAL_CONDUCTIVITY.getID(), + Units.SIEMENS.divide(SIUnits.METRE).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_TORQUE.getID(), Units.NEWTON.multiply(SIUnits.METRE).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_RESISTIVITY.getID(), Units.OHM.multiply(SIUnits.METRE).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRIC_DIPOLEMOMENT.getID(), + Units.COULOMB.multiply(SIUnits.METRE).toString()); + // use definition based on SI units (just rewrite Vm to V*m); + // another common definition uses C, to be handled in encoder + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID(), Units.VOLT.multiply(SIUnits.METRE).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_MAGNETIC_MOMENT.getID(), + Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString()); + DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTROMAGNETIC_MOMENT.getID(), + Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString()); + + // 64 bit signed (DPT 29) + DPT_UNIT_MAP.put(DPTXlator64BitSigned.DPT_REACTIVE_ENERGY.getID(), Units.VAR_HOUR.toString()); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUtil.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUtil.java new file mode 100644 index 00000000000..f3c6823dc9b --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUtil.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.dpt; + +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +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.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; +import tuwien.auto.calimero.dptxlator.DPTXlatorString; + +/** + * This class provides support to determine compatibility between KNX DPTs and openHAB data types + * + * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DPTUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(DPTUtil.class); + + // DPT: "123.001", 1-3 digits main type (no leading zero), optional sub-type 3-4 digits (leading zeros allowed) + public static final Pattern DPT_PATTERN = Pattern.compile("^(?

[1-9][0-9]{0,2})(?:\\.(?\\d{3,5}))?$"); + + // used to map vendor-specific data to standard DPT + public static final Map NORMALIZED_DPT = Map.of(// + "232.60000", "232.600"); + + // fall back if no specific type is defined in DPT_TYPE_MAP + private static final Map>> DPT_MAIN_TYPE_MAP = Map.ofEntries( // + Map.entry("1", Set.of(OnOffType.class)), // + Map.entry("2", Set.of(DecimalType.class)), // + Map.entry("3", Set.of(IncreaseDecreaseType.class)), // + Map.entry("4", Set.of(StringType.class)), // + Map.entry("5", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("6", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("7", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("8", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("9", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("10", Set.of(DateTimeType.class)), // + Map.entry("11", Set.of(DateTimeType.class)), // + Map.entry("12", Set.of(DecimalType.class)), // + Map.entry("13", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("14", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("16", Set.of(StringType.class)), // + Map.entry("17", Set.of(DecimalType.class)), // + Map.entry("18", Set.of(DecimalType.class)), // + Map.entry("19", Set.of(DateTimeType.class)), // + Map.entry("20", Set.of(StringType.class)), // + Map.entry("21", Set.of(StringType.class)), // + Map.entry("22", Set.of(StringType.class)), // + Map.entry("28", Set.of(StringType.class)), // + Map.entry("29", Set.of(QuantityType.class, DecimalType.class)), // + Map.entry("229", Set.of(DecimalType.class)), // + Map.entry("232", Set.of(HSBType.class)), // + Map.entry("242", Set.of(HSBType.class)), // + Map.entry("251", Set.of(HSBType.class, PercentType.class))); + + // compatible types for full DPTs + private static final Map>> DPT_TYPE_MAP = Map.ofEntries( + Map.entry(DPTXlatorBoolean.DPT_UPDOWN.getID(), Set.of(UpDownType.class)), // + Map.entry(DPTXlatorBoolean.DPT_OPENCLOSE.getID(), Set.of(OpenClosedType.class)), // + Map.entry(DPTXlatorBoolean.DPT_START.getID(), Set.of(StopMoveType.class)), // + Map.entry(DPTXlatorBoolean.DPT_WINDOW_DOOR.getID(), Set.of(OpenClosedType.class)), // + Map.entry(DPTXlatorBoolean.DPT_SCENE_AB.getID(), Set.of(DecimalType.class)), // + Map.entry(DPTXlator3BitControlled.DPT_CONTROL_BLINDS.getID(), Set.of(UpDownType.class)), // + Map.entry(DPTXlator8BitUnsigned.DPT_SCALING.getID(), + Set.of(QuantityType.class, DecimalType.class, PercentType.class)), // + Map.entry(DPTXlator8BitSigned.DPT_STATUS_MODE3.getID(), Set.of(StringType.class)), // + Map.entry(DPTXlatorString.DPT_STRING_8859_1.getID(), Set.of(StringType.class)), // + Map.entry(DPTXlatorString.DPT_STRING_ASCII.getID(), Set.of(StringType.class))); + + private DPTUtil() { + // prevent instantiation + } + + /** + * get allowed openHAB types for given DPT + * + * @param dptId the datapoint type id + * @return Set of supported openHAB types (command or state) + */ + public static Set> getAllowedTypes(String dptId) { + Set> allowedTypes = DPT_TYPE_MAP.get(dptId); + if (allowedTypes == null) { + Matcher m = DPT_PATTERN.matcher(dptId); + if (!m.matches()) { + LOGGER.warn("getAllowedTypes couldn't identify main number in dptID '{}'", dptId); + return Set.of(); + } + + allowedTypes = DPT_MAIN_TYPE_MAP.getOrDefault(m.group("main"), Set.of()); + } + return allowedTypes; + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java deleted file mode 100644 index 5b2766f8068..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java +++ /dev/null @@ -1,1174 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.dpt; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.knx.internal.KNXTypeMapper; -import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.DecimalType; -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.OpenClosedType; -import org.openhab.core.library.types.PercentType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StopMoveType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.types.UpDownType; -import org.openhab.core.types.Type; -import org.openhab.core.types.UnDefType; -import org.osgi.service.component.annotations.Component; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import tuwien.auto.calimero.KNXException; -import tuwien.auto.calimero.KNXFormatException; -import tuwien.auto.calimero.KNXIllegalArgumentException; -import tuwien.auto.calimero.datapoint.Datapoint; -import tuwien.auto.calimero.dptxlator.DPT; -import tuwien.auto.calimero.dptxlator.DPTXlator; -import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled; -import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat; -import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned; -import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; -import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat; -import tuwien.auto.calimero.dptxlator.DPTXlator4ByteSigned; -import tuwien.auto.calimero.dptxlator.DPTXlator4ByteUnsigned; -import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned; -import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned; -import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; -import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; -import tuwien.auto.calimero.dptxlator.DPTXlatorDate; -import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime; -import tuwien.auto.calimero.dptxlator.DPTXlatorRGB; -import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl; -import tuwien.auto.calimero.dptxlator.DPTXlatorSceneNumber; -import tuwien.auto.calimero.dptxlator.DPTXlatorString; -import tuwien.auto.calimero.dptxlator.DPTXlatorTime; -import tuwien.auto.calimero.dptxlator.DPTXlatorUtf8; -import tuwien.auto.calimero.dptxlator.TranslatorTypes; - -/** - * This class provides type mapping between all openHAB core types and KNX data point types. - * - * Each 'MainType' delivered from calimero, has a default mapping - * for all it's children to an openHAB Typeclass. - * All these 'MainType' mapping's are put into 'dptMainTypeMap'. - * - * Default 'MainType' mapping's we can override by a specific mapping. - * All specific mapping's are put into 'dptTypeMap'. - * - * If for a 'MainType' there is currently no specific mapping registered, - * you can find a commented example line, with it's correct 'DPTXlator' class. - * - * @author Kai Kreuzer - initial contribution - * @author Volker Daube - improvements - * @author Jan N. Klug - improvements - * @author Helmut Lehmeyer - Java8, generic DPT Mapper - */ -@NonNullByDefault -@Component -public class KNXCoreTypeMapper implements KNXTypeMapper { - - private final Logger logger = LoggerFactory.getLogger(KNXCoreTypeMapper.class); - - private static final String TIME_DAY_FORMAT = new String("EEE, HH:mm:ss"); - private static final String TIME_FORMAT = new String("HH:mm:ss"); - private static final String DATE_FORMAT = new String("yyyy-MM-dd"); - - /** - * stores the openHAB type class for (supported) KNX datapoint types in a generic way. - * dptTypeMap stores more specific type class and exceptions. - */ - private final Map> dptMainTypeMap; - - /** stores the openHAB type class for all (supported) KNX datapoint types */ - private final Map> dptTypeMap; - - /** stores the default KNX DPT to use for each openHAB type */ - private final Map, String> defaultDptMap; - - public KNXCoreTypeMapper() { - @SuppressWarnings("unused") - final List> xlators = Arrays.> asList(DPTXlator1BitControlled.class, - DPTXlator2ByteFloat.class, DPTXlator2ByteUnsigned.class, DPTXlator3BitControlled.class, - DPTXlator4ByteFloat.class, DPTXlator4ByteSigned.class, DPTXlator4ByteUnsigned.class, - DPTXlator64BitSigned.class, DPTXlator8BitSigned.class, DPTXlator8BitUnsigned.class, - DPTXlatorBoolean.class, DPTXlatorDate.class, DPTXlatorDateTime.class, DPTXlatorRGB.class, - DPTXlatorSceneControl.class, DPTXlatorSceneNumber.class, DPTXlatorString.class, DPTXlatorTime.class, - DPTXlatorUtf8.class); - - dptTypeMap = new HashMap<>(); - dptMainTypeMap = new HashMap<>(); - - /** - * MainType: 1 - * 1.000: General bool - * 1.001: DPT_Switch values: 0 = off 1 = on - * 1.002: DPT_Bool values: 0 = false 1 = true - * 1.003: DPT_Enable values: 0 = disable 1 = enable - * 1.004: DPT_Ramp values: 0 = no ramp 1 = ramp - * 1.005: DPT_Alarm values: 0 = no alarm 1 = alarm - * 1.006: DPT_BinaryValue values: 0 = low 1 = high - * 1.007: DPT_Step values: 0 = decrease 1 = increase - * 1.008: DPT_UpDown values: 0 = up 1 = down - * 1.009: DPT_OpenClose values: 0 = open 1 = close - * 1.010: DPT_Start values: 0 = stop 1 = start - * 1.011: DPT_State values: 0 = inactive 1 = active - * 1.012: DPT_Invert values: 0 = not inverted 1 = inverted - * 1.013: DPT_DimSendStyle values: 0 = start/stop 1 = cyclic - * 1.014: DPT_InputSource values: 0 = fixed 1 = calculated - * 1.015: DPT_Reset values: 0 = no action 1 = reset - * 1.016: DPT_Ack values: 0 = no action 1 = acknowledge - * 1.017: DPT_Trigger values: 0 = trigger 1 = trigger - * 1.018: DPT_Occupancy values: 0 = not occupied 1 = occupied - * 1.019: DPT_Window_Door values: 0 = closed 1 = open - * 1.021: DPT_LogicalFunction values: 0 = OR 1 = AND - * 1.022: DPT_Scene_AB values: 0 = scene A 1 = scene B - * 1.023: DPT_ShutterBlinds_Mode values: 0 = only move up/down 1 = move up/down + step-stop - * 1.100: DPT_Heat/Cool values: 0 = cooling 1 = heating - */ - dptMainTypeMap.put(1, OnOffType.class); - /** Exceptions Datapoint Types "B1", Main number 1 */ - dptTypeMap.put(DPTXlatorBoolean.DPT_UPDOWN.getID(), UpDownType.class); - dptTypeMap.put(DPTXlatorBoolean.DPT_OPENCLOSE.getID(), OpenClosedType.class); - dptTypeMap.put(DPTXlatorBoolean.DPT_START.getID(), StopMoveType.class); - dptTypeMap.put(DPTXlatorBoolean.DPT_WINDOW_DOOR.getID(), OpenClosedType.class); - dptTypeMap.put(DPTXlatorBoolean.DPT_SCENE_AB.getID(), DecimalType.class); - - /** - * MainType: 2 - * 2.001: DPT_Switch_Control values: 0 = off 1 = on - * 2.002: DPT_Bool_Control values: 0 = false 1 = true - * 2.003: DPT_Enable_Control values: 0 = disable 1 = enable - * 2.004: DPT_Ramp_Control values: 0 = no ramp 1 = ramp - * 2.005: DPT_Alarm_Control values: 0 = no alarm 1 = alarm - * 2.006: DPT_BinaryValue_Control values: 0 = low 1 = high - * 2.007: DPT_Step_Control values: 0 = decrease 1 = increase - * 2.008: DPT_Direction1_Control values: 0 = up 1 = down - * 2.009: DPT_Direction2_Control values: 0 = open 1 = close - * 2.010: DPT_Start_Control values: 0 = stop 1 = start - * 2.011: DPT_State_Control values: 0 = inactive 1 = active - * 2.012: DPT_Invert_Control values: 0 = not inverted 1 = inverted - */ - dptMainTypeMap.put(2, DecimalType.class); - /** Exceptions Datapoint Types "B2", Main number 2 */ - // Example: dptTypeMap.put(DPTXlator1BitControlled.DPT_SWITCH_CONTROL.getID(), DecimalType.class); - - /** - * MainType: 3 - * 3.007: DPT_Control_Dimming values: 0 = decrease 1 = increase - * 3.008: DPT_Control_Blinds values: 0 = up 1 = down - */ - dptMainTypeMap.put(3, IncreaseDecreaseType.class); - /** Exceptions Datapoint Types "B1U3", Main number 3 */ - dptTypeMap.put(DPTXlator3BitControlled.DPT_CONTROL_BLINDS.getID(), UpDownType.class); - - /** - * MainType: 4 - * 4.001: DPT_Char_ASCII - * 4.002: DPT_Char_8859_1 - */ - dptMainTypeMap.put(4, StringType.class); - - /** - * MainType: 5 - * 5.000: General byte - * 5.001: DPT_Scaling values: 0...100 % - * 5.003: DPT_Angle values: 0...360 ° - * 5.004: DPT_Percent_U8 (8 Bit) values: 0...255 % - * 5.005: DPT_DecimalFactor values: 0...255 ratio - * 5.006: DPT_Tariff values: 0...254 - * 5.010: DPT_Value_1_Ucount Unsigned count values: 0...255 counter pulses - */ - dptMainTypeMap.put(5, DecimalType.class); - /** Exceptions Types "8-Bit Unsigned Value", Main number 5 */ - dptTypeMap.put(DPTXlator8BitUnsigned.DPT_SCALING.getID(), PercentType.class); - dptTypeMap.put(DPTXlator8BitUnsigned.DPT_PERCENT_U8.getID(), PercentType.class); - - /** - * MainType: 6 - * 6.001: DPT_Percent_V8 (8 Bit) values: -128...127 % - * 6.010: DPT_Value_1_Count values: signed -128...127 counter pulses - * 6.020: DPT_Status_Mode3 with mode values: 0/0/0/0/0 0...1/1/1/1/1 2 - */ - dptMainTypeMap.put(6, DecimalType.class); - /** Exceptions Datapoint Types "8-Bit Signed Value", Main number 6 */ - dptTypeMap.put(DPTXlator8BitSigned.DPT_PERCENT_V8.getID(), PercentType.class); - dptTypeMap.put(DPTXlator8BitSigned.DPT_STATUS_MODE3.getID(), StringType.class); - - /** - * MainType: 7 - * 7.000: General unsigned integer - * 7.001: DPT_Value_2_Ucount values: 0...65535 pulses - * 7.002: DPT_TimePeriodMsec values: 0...65535 res 1 ms - * 7.003: DPT_TimePeriod10MSec values: 0...655350 res 10 ms - * 7.004: DPT_TimePeriod100MSec values: 0...6553500 res 100 ms - * 7.005: DPT_TimePeriodSec values: 0...65535 s - * 7.006: DPT_TimePeriodMin values: 0...65535 min - * 7.007: DPT_TimePeriodHrs values: 0...65535 h - * 7.010: DPT_PropDataType values: 0...65535 - * 7.011: DPT_Length_mm values: 0...65535 mm - * 7.012: DPT_UElCurrentmA values: 0...65535 mA - * 7.013: DPT_Brightness values: 0...65535 lx - * 7.600: DPT_Colour_Temperature values: 0...65535 K, 2000K 3000K 5000K 8000K - */ - dptMainTypeMap.put(7, DecimalType.class); - /** Exceptions Datapoint Types "2-Octet Unsigned Value", Main number 7 */ - dptTypeMap.put(DPTXlator2ByteFloat.DPT_HUMIDITY.getID(), PercentType.class); - - /** - * MainType: 8 - * 8.000: General integer - * 8.001: DPT_Value_2_Count - * 8.002: DPT_DeltaTimeMsec - * 8.003: DPT_DeltaTime10MSec - * 8.004: DPT_DeltaTime100MSec - * 8.005: DPT_DeltaTimeSec - * 8.006: DPT_DeltaTimeMin - * 8.007: DPT_DeltaTimeHrs - * 8.010: DPT_Percent_V16 - * 8.011: DPT_Rotation_Angle - * 8.012: DPT_Length_m - */ - dptMainTypeMap.put(8, DecimalType.class); - - /** - * MainType: 9 - * 9.000: General float - * 9.001: DPT_Value_Temp values: -273...+670760 °C - * 9.002: DPT_Value_Tempd values: -670760...+670760 K - * 9.003: DPT_Value_Tempa values: -670760...+670760 K/h - * 9.004: DPT_Value_Lux values: 0...+670760 lx - * 9.005: DPT_Value_Wsp values: 0...+670760 m/s - * 9.006: DPT_Value_Pres values: 0...+670760 Pa - * 9.007: DPT_Value_Humidity values: 0...+670760 % - * 9.008: DPT_Value_AirQuality values: 0...+670760 ppm - * 9.010: DPT_Value_Time1 values: -670760...+670760 s - * 9.011: DPT_Value_Time2 values: -670760...+670760 ms - * 9.020: DPT_Value_Volt values: -670760...+670760 mV - * 9.021: DPT_Value_Curr values: -670760...+670760 mA - * 9.022: DPT_PowerDensity values: -670760...+670760 W/m² - * 9.023: DPT_KelvinPerPercent values: -670760...+670760 K/% - * 9.024: DPT_Power values: -670760...+670760 kW - * 9.025: DPT_Value_Volume_Flow values: -670760...+670760 l/h - * 9.026: DPT_Rain_Amount values: -671088.64...670760.96 l/m² - * 9.027: DPT_Value_Temp_F values: -459.6...670760.96 °F - * 9.028: DPT_Value_Wsp_kmh values: 0...670760.96 km/h - * 9.029: DPT_Value_Absolute_Humidity: 0...670760 g/m³ - * 9.030: DPT_Concentration_μgm3: 0...670760 µg/m³ - */ - dptMainTypeMap.put(9, DecimalType.class); - /** Exceptions Datapoint Types "2-Octet Float Value", Main number 9 */ - dptTypeMap.put(DPTXlator2ByteFloat.DPT_HUMIDITY.getID(), PercentType.class); - - /** - * MainType: 10 - * 10.001: DPT_TimeOfDay values: 1 = Monday...7 = Sunday, 0 = no-day, 00:00:00 Sun, 23:59:59 dow, hh:mm:ss - */ - dptMainTypeMap.put(10, DateTimeType.class); - /** Exceptions Datapoint Types "Time", Main number 10 */ - // Example: dptTypeMap.put(DPTXlatorTime.DPT_TIMEOFDAY.getID(), DateTimeType.class); - - /** - * MainType: 11 - * 11.001: DPT_Date values: 1990-01-01...2089-12-31, yyyy-mm-dd - */ - dptMainTypeMap.put(11, DateTimeType.class); - /** Exceptions Datapoint Types “Date”", Main number 11 */ - // Example: dptTypeMap.put(DPTXlatorDate.DPT_DATE.getID(), DateTimeType.class); - - /** - * MainType: 12 - * 12.000: General unsigned long - * 12.001: DPT_Value_4_Ucount values: 0...4294967295 counter pulses - * 12.100: DPT_LongTimePeriod_Sec values: 0...4294967295 s - * 12.101: DPT_LongTimePeriod_Min values: 0...4294967295 min - * 12.102: DPT_LongTimePeriod_Hrs values: 0...4294967295 h - * 12.1200: DPT_VolumeLiquid_Litre values: 0..4294967295 l - * 12.1201: DPT_Volume_m3 values: 0..4294967295 m3 - */ - dptMainTypeMap.put(12, DecimalType.class); - /** Exceptions Datapoint Types "4-Octet Unsigned Value", Main number 12 */ - // Example: dptTypeMap.put(DPTXlator4ByteUnsigned.DPT_VALUE_4_UCOUNT.getID(), DecimalType.class); - - /** - * MainType: 13 - * 13.000: General long - * 13.001: DPT_Value_4_Count values: -2147483648...2147483647 counter pulses - * 13.002: DPT_FlowRate_m3h values: -2147483648...2147483647 m3/h - * 13.010: DPT_ActiveEnergy values: -2147483648...2147483647 Wh - * 13.011: DPT_ApparantEnergy values: -2147483648...2147483647 VAh - * 13.012: DPT_ReactiveEnergy values: -2147483648...2147483647 VARh - * 13.013: DPT_ActiveEnergy_kWh values: -2147483648...2147483647 kWh - * 13.014: DPT_ApparantEnergy_kVAh values: -2147483648...2147483647 kVAh - * 13.015: DPT_ReactiveEnergy_kVARh values: -2147483648...2147483647 kVAR - * 13.016: DPT_ActiveEnergy_MWh4 values: -2147483648...2147483647 MWh - * 13.100: DPT_LongDeltaTimeSec values: -2147483648...2147483647 s - * 13.1200: DPT_DeltaVolumeLiquid_Litre values: -2147483648...2147483647 l - * 13.1201: DPT_DeltaVolume_m3 values: -2147483648...2147483647 m³ - */ - dptMainTypeMap.put(13, DecimalType.class); - /** Exceptions Datapoint Types "4-Octet Signed Value", Main number 13 */ - // Example: dptTypeMap.put(DPTXlator4ByteSigned.DPT_COUNT.getID(), DecimalType.class); - - /** - * MainType: 14, Range: [-3.40282347e+38f...3.40282347e+38f] - * 14.000: Acceleration, values: ms⁻² - * 14.001: Acceleration, angular, values: rad s⁻² - * 14.002: Activation energy, values: J/mol - * 14.003: Activity, values: s⁻¹ - * 14.004: Mol, values: mol - * 14.005: Amplitude, values: - * 14.006: Angle, values: rad - * 14.007: Angle, values: ° - * 14.008: Momentum, values: Js - * 14.009: Angular velocity, values: rad/s - * 14.010: Area, values: m² - * 14.011: Capacitance, values: F - * 14.012: Charge density (surface), values: C m⁻² - * 14.013: Charge density (volume), values: C m⁻³ - * 14.014: Compressibility, values: m²/N - * 14.015: Conductance, values: Ω⁻¹ - * 14.016: Conductivity, electrical, values: Ω⁻¹m⁻¹ - * 14.017: Density, values: kg m⁻³ - * 14.018: Electric charge, values: C - * 14.019: Electric current, values: A - * 14.020: Electric current density, values: A m⁻² - * 14.021: Electric dipole moment, values: Cm - * 14.022: Electric displacement, values: C m⁻² - * 14.023: Electric field strength, values: V/m - * 14.024: Electric flux, values: Vm - * 14.025: Electric flux density, values: C m⁻² - * 14.026: Electric polarization, values: C m⁻² - * 14.027: Electric potential, values: V - * 14.028: Electric potential difference, values: V - * 14.029: Electromagnetic moment, values: A m² - * 14.030: Electromotive force, values: V - * 14.031: Energy, values: J - * 14.032: Force, values: N - * 14.033: Frequency, values: Hz - * 14.034: Frequency, angular, values: rad/s - * 14.035: Heat capacity, values: J/K - * 14.036: Heat flow rate, values: W - * 14.037: Heat quantity, values: J - * 14.038: Impedance, values: Ω - * 14.039: Length, values: m - * 14.040: Quantity of Light, values: J - * 14.041: Luminance, values: cd m⁻² - * 14.042: Luminous flux, values: lm - * 14.043: Luminous intensity, values: cd - * 14.044: Magnetic field strength, values: A/m - * 14.045: Magnetic flux, values: Wb - * 14.046: Magnetic flux density, values: T - * 14.047: Magnetic moment, values: A m² - * 14.048: Magnetic polarization, values: T - * 14.049: Magnetization, values: A/m - * 14.050: Magneto motive force, values: A - * 14.051: Mass, values: kg - * 14.052: Mass flux, values: kg/s - * 14.053: Momentum, values: N/s - * 14.054: Phase angle, radiant, values: rad - * 14.055: Phase angle, degree, values: ° - * 14.056: Power, values: W - * 14.057: Power factor, values: - * 14.058: Pressure, values: Pa - * 14.059: Reactance, values: Ω - * 14.060: Resistance, values: Ω - * 14.061: Resistivity, values: Ωm - * 14.062: Self inductance, values: H - * 14.063: Solid angle, values: sr - * 14.064: Sound intensity, values: W m⁻² - * 14.065: Speed, values: m/s - * 14.066: Stress, values: Pa - * 14.067: Surface tension, values: N/m - * 14.068: Temperature in Celsius Degree, values: °C - * 14.069: Temperature, absolute, values: K - * 14.070: Temperature difference, values: K - * 14.071: Thermal capacity, values: J/K - * 14.072: Thermal conductivity, values: W/m K⁻¹ - * 14.073: Thermoelectric power, values: V/K - * 14.074: Time, values: s - * 14.075: Torque, values: Nm - * 14.076: Volume, values: m³ - * 14.077: Volume flux, values: m³/s - * 14.078: Weight, values: N - * 14.079: Work, values: J - * 14.080: apparent power: VA - */ - dptMainTypeMap.put(14, DecimalType.class); - /** Exceptions Datapoint Types "4-Octet Float Value", Main number 14 */ - // Example: dptTypeMap.put(DPTXlator4ByteFloat.DPT_ACCELERATION_ANGULAR.getID(), DecimalType.class); - - /** - * MainType: 16 - * 16.000: ASCII string - * 16.001: ISO-8859-1 string (Latin 1) - */ - dptMainTypeMap.put(16, StringType.class); - /** Exceptions Datapoint Types "String", Main number 16 */ - dptTypeMap.put(DPTXlatorString.DPT_STRING_8859_1.getID(), StringType.class); - dptTypeMap.put(DPTXlatorString.DPT_STRING_ASCII.getID(), StringType.class); - - /** - * MainType: 17 - * 17.001: Scene Number, values: 0...63 - */ - dptMainTypeMap.put(17, DecimalType.class); - /** Exceptions Datapoint Types "Scene Number", Main number 17 */ - // Example: dptTypeMap.put(DPTXlatorSceneNumber.DPT_SCENE_NUMBER.getID(), DecimalType.class); - - /** - * MainType: 18 - * 18.001: Scene Control, values: 0...63, 0 = activate, 1 = learn - */ - dptMainTypeMap.put(18, DecimalType.class); - /** Exceptions Datapoint Types "Scene Control", Main number 18 */ - // Example: dptTypeMap.put(DPTXlatorSceneControl.DPT_SCENE_CONTROL.getID(), DecimalType.class); - - /** - * MainType: 19 - * 19.001: Date with time, values: 0 = 1900, 255 = 2155, 01/01 00:00:00, 12/31 24:00:00 yr/mth/day hr:min:sec - */ - dptMainTypeMap.put(19, DateTimeType.class); - /** Exceptions Datapoint Types "DateTime", Main number 19 */ - // Example: dptTypeMap.put(DPTXlatorDateTime.DPT_DATE_TIME.getID(), DateTimeType.class); - - /** - * MainType: 20 - * 20.001: System Clock Mode, enumeration [0..2] - * 20.002: Building Mode, enumeration [0..2] - * 20.003: Occupancy Mode, enumeration [0..2] - * 20.004: Priority, enumeration [0..3] - * 20.005: Light Application Mode, enumeration [0..2] - * 20.006: Application Area, enumeration [0..14] - * 20.007: Alarm Class Type, enumeration [0..3] - * 20.008: PSU Mode, enumeration [0..2] - * 20.011: Error Class System, enumeration [0..18] - * 20.012: Error Class HVAC, enumeration [0..4] - * 20.013: Time Delay, enumeration [0..25] - * 20.014: Beaufort Wind Force Scale, enumeration [0..12] - * 20.017: Sensor Select, enumeration [0..4] - * 20.020: Actuator Connect Type, enumeration [1..2] - * 20.100: Fuel Type, enumeration [0..3] - * 20.101: Burner Type, enumeration [0..3] - * 20.102: HVAC Mode, enumeration [0..4] - * 20.103: DHW Mode, enumeration [0..4] - * 20.104: Load Priority, enumeration [0..2] - * 20.105: HVAC Control Mode, enumeration [0..20] - * 20.106: HVAC Emergency Mode, enumeration [0..5] - * 20.107: Changeover Mode, enumeration [0..2] - * 20.108: Valve Mode, enumeration [1..5] - * 20.109: Damper Mode, enumeration [1..4] - * 20.110: Heater Mode, enumeration [1..3] - * 20.111: Fan Mode, enumeration [0..2] - * 20.112: Master/Slave Mode, enumeration [0..2] - * 20.113: Status Room Setpoint, enumeration [0..2] - * 20.114: Metering Device Type, enumeration [0..41/255] - * 20.120: Air Damper Actuator Type, enumeration [1..2] - * 20.121: Backup Mode, enumeration [0..1] - * 20.122: Start Synchronization, enumeration [0..2] - * 20.600: Behavior Lock/Unlock, enumeration [0..6] - * 20.601: Behavior Bus Power Up/Down, enumeration [0..4] - * 20.602: DALI Fade Time, enumeration [0..15] - * 20.603: Blinking Mode, enumeration [0..2] - * 20.604: Light Control Mode, enumeration [0..1] - * 20.605: Switch PB Model, enumeration [1..2] - * 20.606: PB Action, enumeration [0..3] - * 20.607: Dimm PB Model, enumeration [1..4] - * 20.608: Switch On Mode, enumeration [0..2] - * 20.609: Load Type Set, enumeration [0..2] - * 20.610: Load Type Detected, enumeration [0..3] - * 20.801: SAB Except Behavior, enumeration [0..4] - * 20.802: SAB Behavior Lock/Unlock, enumeration [0..6] - * 20.803: SSSB Mode, enumeration [1..4] - * 20.804: Blinds Control Mode, enumeration [0..1] - * 20.1000: Comm Mode, enumeration [0..255] - * 20.1001: Additional Info Type, enumeration [0..7] - * 20.1002: RF Mode Select, enumeration [0..2] - * 20.1003: RF Filter Select, enumeration [0..3] - * 20.1200: M-Bus Breaker/Valve State, enumeration [0..255] - * 20.1202: Gas Measurement Condition, enumeration [0..3] - * - */ - dptMainTypeMap.put(20, StringType.class); - /** Exceptions Datapoint Types, Main number 20 */ - // Example since calimero 2.4: dptTypeMap.put(DPTXlator8BitEnum.DptSystemClockMode.getID(), StringType.class); - - /** - * MainType: 21 - * 21.001: General Status, values: 0...31 - * 21.002: Device Control, values: 0...7 - * 21.100: Forcing Signal, values: 0...255 - * 21.101: Forcing Signal Cool, values: 0...1 - * 21.102: Room Heating Controller Status, values: 0...255 - * 21.103: Solar Dhw Controller Status, values: 0...7 - * 21.104: Fuel Type Set, values: 0...7 - * 21.105: Room Cooling Controller Status, values: 0...1 - * 21.106: Ventilation Controller Status, values: 0...15 - * 21.601: Light Actuator Error Info, values: 0...127 - * 21.1000: R F Comm Mode Info, values: 0...7 - * 21.1001: R F Filter Modes, values: 0...7 - * 21.1010: Channel Activation State, values: 0...255 - */ - dptMainTypeMap.put(21, StringType.class); - /** Exceptions Datapoint Types, Main number 21 */ - // Example since calimero 2.4: dptTypeMap.put(DptXlator8BitSet.DptGeneralStatus.getID(), StringType.class); - - /** - * MainType: 28 - * 28.001: UTF-8 - */ - dptMainTypeMap.put(28, StringType.class); - /** Exceptions Datapoint Types "String" UTF-8, Main number 28 */ - // Example: dptTypeMap.put(DPTXlatorUtf8.DPT_UTF8.getID(), StringType.class); - - /** - * MainType: 29 - * 29.010: Active Energy, values: -9223372036854775808...9223372036854775807 Wh - * 29.011: Apparent energy, values: -9223372036854775808...9223372036854775807 VAh - * 29.012: Reactive energy, values: -9223372036854775808...9223372036854775807 VARh - */ - dptMainTypeMap.put(29, DecimalType.class); - /** Exceptions Datapoint Types "64-Bit Signed Value", Main number 29 */ - // Example: dptTypeMap.put(DPTXlator64BitSigned.DPT_ACTIVE_ENERGY.getID(), DecimalType.class); - - /** - * MainType: 229 - * 229.001: Metering Value, values: -2147483648...2147483647 - */ - dptMainTypeMap.put(229, DecimalType.class); - /** Exceptions Datapoint Types "4-Octet Signed Value", Main number 229 */ - // Example: dptTypeMap.put(DptXlatorMeteringValue.DptMeteringValue.getID(), DecimalType.class); - - /** - * MainType: 232, 3 bytes - * 232.600: DPT_Colour_RGB, values: 0 0 0...255 255 255, r g b - */ - dptMainTypeMap.put(232, HSBType.class); - /** Exceptions Datapoint Types "RGB Color", Main number 232 */ - // Example: dptTypeMap.put(DPTXlatorRGB.DPT_RGB.getID(), HSBType.class); - - defaultDptMap = new HashMap<>(); - defaultDptMap.put(OnOffType.class, DPTXlatorBoolean.DPT_SWITCH.getID()); - defaultDptMap.put(UpDownType.class, DPTXlatorBoolean.DPT_UPDOWN.getID()); - defaultDptMap.put(StopMoveType.class, DPTXlatorBoolean.DPT_START.getID()); - defaultDptMap.put(OpenClosedType.class, DPTXlatorBoolean.DPT_WINDOW_DOOR.getID()); - defaultDptMap.put(IncreaseDecreaseType.class, DPTXlator3BitControlled.DPT_CONTROL_DIMMING.getID()); - defaultDptMap.put(PercentType.class, DPTXlator8BitUnsigned.DPT_SCALING.getID()); - defaultDptMap.put(DecimalType.class, DPTXlator2ByteFloat.DPT_TEMPERATURE.getID()); - defaultDptMap.put(DateTimeType.class, DPTXlatorTime.DPT_TIMEOFDAY.getID()); - defaultDptMap.put(StringType.class, DPTXlatorString.DPT_STRING_8859_1.getID()); - defaultDptMap.put(HSBType.class, DPTXlatorRGB.DPT_RGB.getID()); - } - - /* - * This function computes the target unit for type conversion from OH quantity type to DPT types. - * Calimero library provides units which can be used for most of the DPTs. There are some deviations - * from the OH unit scheme which are handled. - */ - private String quantityTypeToDPTValue(QuantityType qt, int mainNumber, int subNumber, String dpUnit) - throws KNXException { - String targetOhUnit = dpUnit; - double scaleFactor = 1.0; - switch (mainNumber) { - case 7: - switch (subNumber) { - case 3: - case 4: - targetOhUnit = "ms"; - break; - } - break; - case 9: - switch (subNumber) { - // special case: temperature deltas specified in different units - // ignore the offset, but run a conversion to handle prefixes like mK - // scaleFactor is needed to properly handle °F - case 2: { - final String unit = qt.getUnit().toString(); - // find out if the unit is based on °C or K, getSystemUnit() does not help here as it always - // gives "K" - if (unit.contains("°C")) { - targetOhUnit = "°C"; - } else if (unit.contains("°F")) { - targetOhUnit = "°F"; - scaleFactor = 5.0 / 9.0; - } else if (unit.contains("K")) { - targetOhUnit = "K"; - } else { - targetOhUnit = ""; - } - break; - } - case 3: { - final String unit = qt.getUnit().toString(); - if (unit.contains("°C")) { - targetOhUnit = "°C/h"; - } else if (unit.contains("°F")) { - targetOhUnit = "°F/h"; - scaleFactor = 5.0 / 9.0; - } else if (unit.contains("K")) { - targetOhUnit = "K/h"; - } else { - targetOhUnit = ""; - } - break; - } - case 23: { - final String unit = qt.getUnit().toString(); - if (unit.contains("°C")) { - targetOhUnit = "°C/%"; - } else if (unit.contains("°F")) { - targetOhUnit = "°F/%"; - scaleFactor = 5.0 / 9.0; - } else if (unit.contains("K")) { - targetOhUnit = "K/%"; - } else { - targetOhUnit = ""; - } - break; - } - } - break; - case 12: - switch (subNumber) { - case 1200: - // Calimero uses "litre" - targetOhUnit = "l"; - break; - } - break; - case 13: - switch (subNumber) { - case 12: - case 15: - // Calimero uses VARh, OH uses varh - targetOhUnit = targetOhUnit.replace("VARh", "varh"); - break; - case 14: - // OH does not accept kVAh, only VAh - targetOhUnit = targetOhUnit.replace("kVAh", "VAh"); - scaleFactor = 1.0 / 1000.0; - break; - } - break; - - case 14: - targetOhUnit = targetOhUnit.replace("Ω\u207B¹", "S"); - // Calimero uses a special unicode character to specify units like m*s^-2 - // this needs to be rewritten to m/s² - final int posMinus = targetOhUnit.indexOf("\u207B"); - if (posMinus > 0) { - targetOhUnit = targetOhUnit.substring(0, posMinus - 1) + "/" + targetOhUnit.charAt(posMinus - 1) - + targetOhUnit.substring(posMinus + 1); - } - switch (subNumber) { - case 8: - // OH does not support unut Js, need to expand - targetOhUnit = "J*s"; - break; - case 21: - targetOhUnit = "C*m"; - break; - case 24: - targetOhUnit = "C"; - break; - case 29: - case 47: - targetOhUnit = "A*m²"; - break; - case 40: - if (qt.getUnit().toString().contains("J")) { - targetOhUnit = "J"; - } else { - targetOhUnit = "lm*s"; - } - break; - case 61: - targetOhUnit = "Ohm*m"; - break; - case 75: - targetOhUnit = "N*m"; - break; - } - break; - case 29: - switch (subNumber) { - case 12: - // Calimero uses VARh, OH uses varh - targetOhUnit = targetOhUnit.replace("VARh", "varh"); - break; - } - break; - } - // replace e.g. m3 by m³ - targetOhUnit = targetOhUnit.replace("3", "³").replace("2", "²"); - - final QuantityType result = qt.toUnit(targetOhUnit); - if (result == null) { - throw new KNXException("incompatible types: " + qt.getUnit().toString() + ", " + targetOhUnit); - } - return String.valueOf(result.doubleValue() * scaleFactor); - } - - @Override - public @Nullable String toDPTValue(Type type, @Nullable String dptID) { - DPT dpt; - int mainNumber = getMainNumber(dptID); - if (mainNumber == -1) { - logger.error("toDPTValue couldn't identify mainnumber in dptID: {}", dptID); - return null; - } - int subNumber = getSubNumber(dptID); - if (subNumber == -1) { - logger.debug("toType: couldn't identify sub number in dptID: {}.", dptID); - return null; - } - - try { - DPTXlator translator = TranslatorTypes.createTranslator(mainNumber, dptID); - dpt = translator.getType(); - } catch (KNXException e) { - return null; - } - - try { - // check for HSBType first, because it extends PercentType as well - if (type instanceof HSBType) { - switch (mainNumber) { - case 5: - switch (subNumber) { - case 3: // * 5.003: Angle, values: 0...360 ° - return ((HSBType) type).getHue().toString(); - case 1: // * 5.001: Scaling, values: 0...100 % - default: - return ((HSBType) type).getBrightness().toString(); - } - case 232: - switch (subNumber) { - case 600: // 232.600 - HSBType hc = ((HSBType) type); - return "r:" + convertPercentToByte(hc.getRed()) + " g:" - + convertPercentToByte(hc.getGreen()) + " b:" - + convertPercentToByte(hc.getBlue()); - } - default: - HSBType hc = ((HSBType) type); - return "r:" + hc.getRed().intValue() + " g:" + hc.getGreen().intValue() + " b:" - + hc.getBlue().intValue(); - } - } else if (type instanceof OnOffType) { - return type.equals(OnOffType.OFF) ? dpt.getLowerValue() : dpt.getUpperValue(); - } else if (type instanceof UpDownType) { - return type.equals(UpDownType.UP) ? dpt.getLowerValue() : dpt.getUpperValue(); - } else if (type instanceof IncreaseDecreaseType) { - DPT valueDPT = ((DPTXlator3BitControlled.DPT3BitControlled) dpt).getControlDPT(); - return type.equals(IncreaseDecreaseType.DECREASE) ? valueDPT.getLowerValue() + " 5" - : valueDPT.getUpperValue() + " 5"; - } else if (type instanceof OpenClosedType) { - return type.equals(OpenClosedType.CLOSED) ? dpt.getLowerValue() : dpt.getUpperValue(); - } else if (type instanceof StopMoveType) { - return type.equals(StopMoveType.STOP) ? dpt.getLowerValue() : dpt.getUpperValue(); - } else if (type instanceof PercentType) { - return String.valueOf(((DecimalType) type).intValue()); - } else if (type instanceof DecimalType) { - switch (mainNumber) { - case 2: - DPT valueDPT = ((DPTXlator1BitControlled.DPT1BitControlled) dpt).getValueDPT(); - switch (((DecimalType) type).intValue()) { - case 0: - return "0 " + valueDPT.getLowerValue(); - case 1: - return "0 " + valueDPT.getUpperValue(); - case 2: - return "1 " + valueDPT.getLowerValue(); - default: - return "1 " + valueDPT.getUpperValue(); - } - case 18: - int intVal = ((DecimalType) type).intValue(); - if (intVal > 63) { - return "learn " + (intVal - 0x80); - } else { - return "activate " + intVal; - } - default: - return ((DecimalType) type).toBigDecimal().stripTrailingZeros().toPlainString(); - } - } else if (type instanceof StringType) { - return type.toString(); - } else if (type instanceof DateTimeType) { - return formatDateTime((DateTimeType) type, dptID); - } else if (type instanceof QuantityType) { - final QuantityType qt = (QuantityType) type; - return quantityTypeToDPTValue(qt, mainNumber, subNumber, dpt.getUnit()); - } - } catch (Exception e) { - logger.warn("An exception occurred converting type {} to dpt id {}: error message={}", type, dptID, - e.getMessage()); - return null; - } - - logger.debug("toDPTValue: Couldn't convert type {} to dpt id {} (no mapping).", type, dptID); - - return null; - } - - @Override - public @Nullable Type toType(Datapoint datapoint, byte[] data) { - try { - DPTXlator translator = TranslatorTypes.createTranslator(datapoint.getMainNumber(), datapoint.getDPT()); - translator.setData(data); - String value = translator.getValue(); - - String id = translator.getType().getID(); - logger.trace("toType datapoint DPT = {}", datapoint.getDPT()); - - int mainNumber = getMainNumber(id); - if (mainNumber == -1) { - logger.debug("toType: couldn't identify mainnumber in dptID: {}.", id); - return null; - } - int subNumber = getSubNumber(id); - if (subNumber == -1) { - logger.debug("toType: couldn't identify sub number in dptID: {}.", id); - return null; - } - /* - * Following code section deals with specific mapping of values from KNX to openHAB types were the String - * received from the DPTXlator is not sufficient to set the openHAB type or has bugs - */ - switch (mainNumber) { - case 1: - DPTXlatorBoolean translatorBoolean = (DPTXlatorBoolean) translator; - switch (subNumber) { - case 8: - return translatorBoolean.getValueBoolean() ? UpDownType.DOWN : UpDownType.UP; - case 9: - return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED; - case 10: - return translatorBoolean.getValueBoolean() ? StopMoveType.MOVE : StopMoveType.STOP; - case 19: - return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED; - case 22: - return DecimalType.valueOf(translatorBoolean.getValueBoolean() ? "1" : "0"); - default: - return translatorBoolean.getValueBoolean() ? OnOffType.ON : OnOffType.OFF; - } - case 2: - DPTXlator1BitControlled translator1BitControlled = (DPTXlator1BitControlled) translator; - int decValue = (translator1BitControlled.getControlBit() ? 2 : 0) - + (translator1BitControlled.getValueBit() ? 1 : 0); - return new DecimalType(decValue); - case 3: - DPTXlator3BitControlled translator3BitControlled = (DPTXlator3BitControlled) translator; - if (translator3BitControlled.getStepCode() == 0) { - logger.debug("toType: KNX DPT_Control_Dimming: break received."); - return UnDefType.NULL; - } - switch (subNumber) { - case 7: - return translator3BitControlled.getControlBit() ? IncreaseDecreaseType.INCREASE - : IncreaseDecreaseType.DECREASE; - case 8: - return translator3BitControlled.getControlBit() ? UpDownType.DOWN : UpDownType.UP; - } - break; - case 18: - DPTXlatorSceneControl translatorSceneControl = (DPTXlatorSceneControl) translator; - int decimalValue = translatorSceneControl.getSceneNumber(); - if (value.startsWith("learn")) { - decimalValue += 0x80; - } - value = String.valueOf(decimalValue); - - break; - case 19: - DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator; - if (translatorDateTime.isFaultyClock()) { - // Not supported: faulty clock - logger.debug("toType: KNX clock msg ignored: clock faulty bit set, which is not supported"); - return null; - } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) { - // Not supported: "/1/1" (month and day without year) - logger.debug( - "toType: KNX clock msg ignored: no year, but day and month, which is not supported"); - return null; - } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) { - // Not supported: "1900" (year without month and day) - logger.debug( - "toType: KNX clock msg ignored: no day and month, but year, which is not supported"); - return null; - } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE) - && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { - // Not supported: No year, no date and no time - logger.debug("toType: KNX clock msg ignored: no day and month or year, which is not supported"); - return null; - } - - Calendar cal = Calendar.getInstance(); - if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { - // Pure date format, no time information - cal.setTimeInMillis(translatorDateTime.getValueMilliseconds()); - value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); - return DateTimeType.valueOf(value); - } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { - // Pure time format, no date information - cal.clear(); - cal.set(Calendar.HOUR_OF_DAY, translatorDateTime.getHour()); - cal.set(Calendar.MINUTE, translatorDateTime.getMinute()); - cal.set(Calendar.SECOND, translatorDateTime.getSecond()); - value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); - return DateTimeType.valueOf(value); - } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) - && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { - // Date format and time information - cal.setTimeInMillis(translatorDateTime.getValueMilliseconds()); - value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); - return DateTimeType.valueOf(value); - } - break; - } - - Class typeClass = toTypeClass(id); - if (typeClass == null) { - return null; - } - - if (typeClass.equals(PercentType.class)) { - return new PercentType(BigDecimal.valueOf(Math.round(translator.getNumericValue()))); - } - if (typeClass.equals(DecimalType.class)) { - return new DecimalType(translator.getNumericValue()); - } - if (typeClass.equals(StringType.class)) { - return StringType.valueOf(value); - } - - if (typeClass.equals(DateTimeType.class)) { - String date = formatDateTime(value, datapoint.getDPT()); - if (date.isEmpty()) { - logger.debug("toType: KNX clock msg ignored: date object empty {}.", date); - return null; - } else { - return DateTimeType.valueOf(date); - } - } - - if (typeClass.equals(HSBType.class)) { - // value has format of "r: g: b:" - int r = Integer.parseInt(value.split(" ")[0].split(":")[1]); - int g = Integer.parseInt(value.split(" ")[1].split(":")[1]); - int b = Integer.parseInt(value.split(" ")[2].split(":")[1]); - - return HSBType.fromRGB(r, g, b); - } - - } catch (KNXFormatException kfe) { - logger.info("Translator couldn't parse data for datapoint type '{}' (KNXFormatException).", - datapoint.getDPT()); - } catch (KNXIllegalArgumentException kiae) { - logger.info("Translator couldn't parse data for datapoint type '{}' (KNXIllegalArgumentException).", - datapoint.getDPT()); - } catch (KNXException e) { - logger.warn("Failed creating a translator for datapoint type '{}'.", datapoint.getDPT(), e); - } - - return null; - } - - /** - * Converts a datapoint type id into an openHAB type class - * - * @param dptId the datapoint type id - * @return the openHAB type (command or state) class or {@code null} if the datapoint type id is not supported. - */ - @Override - public @Nullable Class toTypeClass(@Nullable String dptId) { - @Nullable - Class ohClass = dptTypeMap.get(dptId); - if (ohClass == null) { - int mainNumber = getMainNumber(dptId); - if (mainNumber == -1) { - logger.debug("Couldn't convert KNX datapoint type id into openHAB type class for dptId: {}.", dptId); - return null; - } - ohClass = dptMainTypeMap.get(mainNumber); - } - return ohClass; - } - - /** - * Converts an openHAB type class into a datapoint type id. - * - * @param typeClass the openHAB type class - * @return the datapoint type id - */ - public @Nullable String toDPTid(Class typeClass) { - return defaultDptMap.get(typeClass); - } - - /** - * Formats the given value according to the datapoint type - * dpt to a String which can be processed by {@link DateTimeType}. - * - * @param value - * @param dpt - * - * @return a formatted String like yyyy-MM-dd'T'HH:mm:ss which - * is target format of the {@link DateTimeType} - */ - private String formatDateTime(String value, @Nullable String dpt) { - Date date = null; - - try { - if (DPTXlatorDate.DPT_DATE.getID().equals(dpt)) { - date = new SimpleDateFormat(DATE_FORMAT).parse(value); - } else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dpt)) { - if (value.contains("no-day")) { - /* - * KNX "no-day" needs special treatment since openHAB's DateTimeType doesn't support "no-day". - * Workaround: remove the "no-day" String, parse the remaining time string, which will result in a - * date of "1970-01-01". - * Replace "no-day" with the current day name - */ - StringBuffer stb = new StringBuffer(value); - int start = stb.indexOf("no-day"); - int end = start + "no-day".length(); - stb.replace(start, end, String.format(Locale.US, "%1$ta", Calendar.getInstance())); - value = stb.toString(); - } - try { - date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value); - } catch (ParseException pe) { - date = new SimpleDateFormat(TIME_FORMAT, Locale.US).parse(value); - } - } - } catch (ParseException pe) { - // do nothing but logging - logger.warn("Could not parse '{}' to a valid date", value); - } - - return date != null ? new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date) : ""; - } - - /** - * Formats the given internal dateType to a knx readable String - * according to the target datapoint type dpt. - * - * @param dateType - * @param dpt the target datapoint type - * - * @return a String which contains either an ISO8601 formatted date (yyyy-mm-dd), - * a formatted 24-hour clock with the day of week prepended (Mon, 12:00:00) or - * a formatted 24-hour clock (12:00:00) - * - * @throws IllegalArgumentException if none of the datapoint types DPT_DATE or - * DPT_TIMEOFDAY has been used. - */ - private static String formatDateTime(DateTimeType dateType, @Nullable String dpt) { - if (DPTXlatorDate.DPT_DATE.getID().equals(dpt)) { - return dateType.format("%tF"); - } else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dpt)) { - return dateType.format(Locale.US, "%1$ta, %1$tT"); - } else if (DPTXlatorDateTime.DPT_DATE_TIME.getID().equals(dpt)) { - return dateType.format(Locale.US, "%tF %1$tT"); - } else { - throw new IllegalArgumentException("Could not format date to datapoint type '" + dpt + "'"); - } - } - - /** - * Retrieves sub number from a DTP ID such as "14.001" - * - * @param dptID String with DPT ID - * @return sub number or -1 - */ - private int getSubNumber(@Nullable String dptID) { - int result = -1; - if (dptID == null) { - throw new IllegalArgumentException("Parameter dptID cannot be null"); - } - - int dptSepratorPosition = dptID.indexOf('.'); - if (dptSepratorPosition > 0) { - try { - result = Integer.parseInt(dptID.substring(dptSepratorPosition + 1, dptID.length())); - } catch (NumberFormatException nfe) { - logger.error("toType couldn't identify main and/or sub number in dptID (NumberFormatException): {}", - dptID); - } catch (IndexOutOfBoundsException ioobe) { - logger.error("toType couldn't identify main and/or sub number in dptID (IndexOutOfBoundsException): {}", - dptID); - } - } - return result; - } - - /** - * Retrieves main number from a DTP ID such as "14.001" - * - * @param dptID String with DPT ID - * @return main number or -1 - */ - private int getMainNumber(@Nullable String dptID) { - int result = -1; - if (dptID == null) { - throw new IllegalArgumentException("Parameter dptID cannot be null"); - } - - int dptSepratorPosition = dptID.indexOf('.'); - if (dptSepratorPosition > 0) { - try { - result = Integer.parseInt(dptID.substring(0, dptSepratorPosition)); - } catch (NumberFormatException nfe) { - logger.error("toType couldn't identify main and/or sub number in dptID (NumberFormatException): {}", - dptID); - } catch (IndexOutOfBoundsException ioobe) { - logger.error("toType couldn't identify main and/or sub number in dptID (IndexOutOfBoundsException): {}", - dptID); - } - } - return result; - } - - /** - * convert 0...100% to 1 byte 0..255 - * - * @param percent - * @return int 0..255 - */ - private int convertPercentToByte(PercentType percent) { - return percent.toBigDecimal().multiply(BigDecimal.valueOf(255)) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).intValue(); - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueDecoder.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueDecoder.java new file mode 100644 index 00000000000..0f1db1307e3 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueDecoder.java @@ -0,0 +1,382 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.dpt; + +import static org.openhab.binding.knx.internal.KNXBindingConstants.disableUoM; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +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.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Type; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.KNXFormatException; +import tuwien.auto.calimero.KNXIllegalArgumentException; +import tuwien.auto.calimero.dptxlator.DPTXlator; +import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled; +import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; +import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; +import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime; +import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl; +import tuwien.auto.calimero.dptxlator.TranslatorTypes; + +/** + * This class decodes raw data received from the KNX bus to an openHAB datatype + * + * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ValueDecoder { + private static final Logger LOGGER = LoggerFactory.getLogger(ValueDecoder.class); + + private static final String TIME_DAY_FORMAT = "EEE, HH:mm:ss"; + private static final String TIME_FORMAT = "HH:mm:ss"; + private static final String DATE_FORMAT = "yyyy-MM-dd"; + // RGB: "r:123 g:123 b:123" value-range: 0-255 + private static final Pattern RGB_PATTERN = Pattern.compile("r:(?\\d+) g:(?\\d+) b:(?\\d+)"); + // RGBW: "100 27 25 12 %", value range: 0-100, invalid values: "-" + private static final Pattern RGBW_PATTERN = Pattern + .compile("(?:(?[\\d,.]+)|-)\\s(?:(?[\\d,.]+)|-)\\s(?:(?[\\d,.]+)|-)\\s(?:(?[\\d,.]+)|-)\\s%"); + // xyY: "(0,123 0,123) 56 %", value range 0-1 for xy (comma as decimal point), 0-100 for Y, invalid values omitted + private static final Pattern XYY_PATTERN = Pattern + .compile("(?:\\((?\\d+(?:,\\d+)?) (?\\d+(?:,\\d+)?)\\))?\\s*(?:(?\\d+(?:,\\d+)?)\\s%)?"); + + /** + * convert the raw value received to the corresponding openHAB value + * + * @param dptId the DPT of the given data + * @param data a byte array containing the value + * @param preferredType the preferred datatype for this conversion + * @return the data converted to an openHAB Type (or null if conversion failed) + */ + public static @Nullable Type decode(String dptId, byte[] data, Class preferredType) { + try { + DPTXlator translator = TranslatorTypes.createTranslator(0, + DPTUtil.NORMALIZED_DPT.getOrDefault(dptId, dptId)); + translator.setData(data); + String value = translator.getValue(); + + String id = dptId; // prefer using the user-supplied DPT + + Matcher m = DPTUtil.DPT_PATTERN.matcher(id); + if (!m.matches() || m.groupCount() != 2) { + LOGGER.trace("User-Supplied DPT '{}' did not match for sub-type, using DPT returned from Translator", + id); + id = translator.getType().getID(); + m = DPTUtil.DPT_PATTERN.matcher(id); + if (!m.matches() || m.groupCount() != 2) { + LOGGER.warn("Couldn't identify main/sub number in dptID '{}'", id); + return null; + } + } + LOGGER.trace("Finally using datapoint DPT = {}", id); + + String mainType = m.group("main"); + String subType = m.group("sub"); + + switch (mainType) { + case "1": + return handleDpt1(subType, translator); + case "2": + DPTXlator1BitControlled translator1BitControlled = (DPTXlator1BitControlled) translator; + int decValue = (translator1BitControlled.getControlBit() ? 2 : 0) + + (translator1BitControlled.getValueBit() ? 1 : 0); + return new DecimalType(decValue); + case "3": + return handleDpt3(subType, translator); + case "10": + return handleDpt10(value); + case "11": + return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN) + .format(new SimpleDateFormat(DATE_FORMAT).parse(value))); + case "18": + DPTXlatorSceneControl translatorSceneControl = (DPTXlatorSceneControl) translator; + int decimalValue = translatorSceneControl.getSceneNumber(); + if (value.startsWith("learn")) { + decimalValue += 0x80; + } + return new DecimalType(decimalValue); + case "19": + return handleDpt19(translator); + case "16": + case "20": + case "21": + case "22": + case "28": + return StringType.valueOf(value); + case "232": + return handleDpt232(value, subType); + case "242": + return handleDpt242(value); + case "251": + return handleDpt251(value, preferredType); + default: + return handleNumericDpt(id, translator, preferredType); + } + } catch (NumberFormatException | KNXFormatException | KNXIllegalArgumentException | ParseException e) { + LOGGER.info("Translator couldn't parse data '{}' for datapoint type '{}' ({}).", data, dptId, e.getClass()); + } catch (KNXException e) { + LOGGER.warn("Failed creating a translator for datapoint type '{}'.", dptId, e); + } + + return null; + } + + private static Type handleDpt1(String subType, DPTXlator translator) { + DPTXlatorBoolean translatorBoolean = (DPTXlatorBoolean) translator; + switch (subType) { + case "008": + return translatorBoolean.getValueBoolean() ? UpDownType.DOWN : UpDownType.UP; + case "009": + case "019": + // This is wrong for DPT 1.009. It should be true -> CLOSE, false -> OPEN, but unfortunately + // can't be fixed without breaking a lot of working installations. + // The documentation has been updated to reflect that. / @J-N-K + return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + case "010": + return translatorBoolean.getValueBoolean() ? StopMoveType.MOVE : StopMoveType.STOP; + case "022": + return DecimalType.valueOf(translatorBoolean.getValueBoolean() ? "1" : "0"); + default: + return OnOffType.from(translatorBoolean.getValueBoolean()); + } + } + + private static @Nullable Type handleDpt3(String subType, DPTXlator translator) { + DPTXlator3BitControlled translator3BitControlled = (DPTXlator3BitControlled) translator; + if (translator3BitControlled.getStepCode() == 0) { + LOGGER.debug("convertRawDataToType: KNX DPT_Control_Dimming: break received."); + return UnDefType.NULL; + } + switch (subType) { + case "007": + return translator3BitControlled.getControlBit() ? IncreaseDecreaseType.INCREASE + : IncreaseDecreaseType.DECREASE; + case "008": + return translator3BitControlled.getControlBit() ? UpDownType.DOWN : UpDownType.UP; + default: + LOGGER.warn("DPT3, subtype '{}' is unknown.", subType); + return null; + } + } + + private static Type handleDpt10(String value) throws ParseException { + if (value.contains("no-day")) { + /* + * KNX "no-day" needs special treatment since openHAB's DateTimeType doesn't support "no-day". + * Workaround: remove the "no-day" String, parse the remaining time string, which will result in a + * date of "1970-01-01". + * Replace "no-day" with the current day name + */ + StringBuilder stb = new StringBuilder(value); + int start = stb.indexOf("no-day"); + int end = start + "no-day".length(); + stb.replace(start, end, String.format(Locale.US, "%1$ta", Calendar.getInstance())); + value = stb.toString(); + } + Date date = null; + try { + date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value); + } catch (ParseException pe) { + date = new SimpleDateFormat(TIME_FORMAT, Locale.US).parse(value); + throw pe; + } + return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date)); + } + + private static @Nullable Type handleDpt19(DPTXlator translator) throws KNXFormatException { + DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator; + if (translatorDateTime.isFaultyClock()) { + // Not supported: faulty clock + LOGGER.debug("KNX clock msg ignored: clock faulty bit set, which is not supported"); + return null; + } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) { + // Not supported: "/1/1" (month and day without year) + LOGGER.debug("KNX clock msg ignored: no year, but day and month, which is not supported"); + return null; + } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) { + // Not supported: "1900" (year without month and day) + LOGGER.debug("KNX clock msg ignored: no day and month, but year, which is not supported"); + return null; + } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && !translatorDateTime.isValidField(DPTXlatorDateTime.DATE) + && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { + // Not supported: No year, no date and no time + LOGGER.debug("KNX clock msg ignored: no day and month or year, which is not supported"); + return null; + } + + Calendar cal = Calendar.getInstance(); + if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { + // Pure date format, no time information + cal.setTimeInMillis(translatorDateTime.getValueMilliseconds()); + String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); + return DateTimeType.valueOf(value); + } else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { + // Pure time format, no date information + cal.clear(); + cal.set(Calendar.HOUR_OF_DAY, translatorDateTime.getHour()); + cal.set(Calendar.MINUTE, translatorDateTime.getMinute()); + cal.set(Calendar.SECOND, translatorDateTime.getSecond()); + String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); + return DateTimeType.valueOf(value); + } else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR) + && translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) { + // Date format and time information + cal.setTimeInMillis(translatorDateTime.getValueMilliseconds()); + String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime()); + return DateTimeType.valueOf(value); + } else { + LOGGER.warn("Failed to convert '{}'", translator.getValue()); + return null; + } + } + + private static @Nullable Type handleDpt232(String value, String subType) { + Matcher rgb = RGB_PATTERN.matcher(value); + if (rgb.matches()) { + int r = Integer.parseInt(rgb.group("r")); + int g = Integer.parseInt(rgb.group("g")); + int b = Integer.parseInt(rgb.group("b")); + + switch (subType) { + case "600": + return HSBType.fromRGB(r, g, b); + case "60000": + // MDT specific: mis-use 232.600 for hsv instead of rgb + DecimalType hue = new DecimalType(coerceToRange(r * 360.0 / 255.0, 0.0, 359.9999)); + PercentType sat = new PercentType(BigDecimal.valueOf(coerceToRange(g / 2.55, 0.0, 100.0))); + PercentType bright = new PercentType(BigDecimal.valueOf(coerceToRange(b / 2.55, 0.0, 100.0))); + return new HSBType(hue, sat, bright); + default: + LOGGER.warn("Unknown subtype '232.{}', no conversion possible.", subType); + return null; + } + } + LOGGER.warn("Failed to convert '{}' (DPT 232): Pattern does not match", value); + return null; + } + + private static @Nullable Type handleDpt242(String value) { + Matcher xyY = XYY_PATTERN.matcher(value); + if (xyY.matches()) { + String stringx = xyY.group("x"); + String stringy = xyY.group("y"); + String stringY = xyY.group("Y"); + + if (stringx != null && stringy != null) { + double x = Double.parseDouble(stringx.replace(",", ".")); + double y = Double.parseDouble(stringy.replace(",", ".")); + if (stringY == null) { + return ColorUtil.xyToHsv(new double[] { x, y }); + } else { + double Y = Double.parseDouble(stringY.replace(",", ".")); + return ColorUtil.xyToHsv(new double[] { x, y, Y }); + } + } + } + LOGGER.warn("Failed to convert '{}' (DPT 242): Pattern does not match", value); + return null; + } + + private static @Nullable Type handleDpt251(String value, Class preferredType) { + Matcher rgbw = RGBW_PATTERN.matcher(value); + if (rgbw.matches()) { + String rString = rgbw.group("r"); + String gString = rgbw.group("g"); + String bString = rgbw.group("b"); + String wString = rgbw.group("w"); + + if (rString != null && gString != null && bString != null && HSBType.class.equals(preferredType)) { + // does not support PercentType and r,g,b valid -> HSBType + int r = coerceToRange((int) (Double.parseDouble(rString.replace(",", ".")) * 2.55), 0, 255); + int g = coerceToRange((int) (Double.parseDouble(gString.replace(",", ".")) * 2.55), 0, 255); + int b = coerceToRange((int) (Double.parseDouble(bString.replace(",", ".")) * 2.55), 0, 255); + + return HSBType.fromRGB(r, g, b); + } else if (wString != null && PercentType.class.equals(preferredType)) { + // does support PercentType and w valid -> PercentType + BigDecimal w = new BigDecimal(wString.replace(",", ".")); + + return new PercentType(w); + } + } + LOGGER.warn("Failed to convert '{}' (DPT 251): Pattern does not match or invalid content", value); + return null; + } + + private static @Nullable Type handleNumericDpt(String id, DPTXlator translator, Class preferredType) + throws KNXFormatException { + Set> allowedTypes = DPTUtil.getAllowedTypes(id); + + double value = translator.getNumericValue(); + if (allowedTypes.contains(PercentType.class) + && (HSBType.class.equals(preferredType) || PercentType.class.equals(preferredType))) { + return new PercentType(BigDecimal.valueOf(Math.round(value))); + } + + if (allowedTypes.contains(QuantityType.class) && !disableUoM) { + String unit = DPTUnits.getUnitForDpt(id); + if (unit != null) { + return new QuantityType<>(value + " " + unit); + } else { + LOGGER.trace("Could not determine unit for DPT '{}', fallback to plain decimal", id); + } + } + + if (allowedTypes.contains(DecimalType.class)) { + return new DecimalType(value); + } + + LOGGER.warn("Failed to convert '{}' (DPT '{}'): no matching type found", value, id); + return null; + } + + private static double coerceToRange(double value, double min, double max) { + return Math.min(Math.max(value, min), max); + } + + private static int coerceToRange(int value, int min, int max) { + return Math.min(Math.max(value, min), max); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueEncoder.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueEncoder.java new file mode 100644 index 00000000000..91e102adade --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueEncoder.java @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.dpt; + +import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Locale; +import java.util.regex.Matcher; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +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.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.Type; +import org.openhab.core.util.ColorUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.dptxlator.DPT; +import tuwien.auto.calimero.dptxlator.DPTXlator; +import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled; +import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat; +import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat; +import tuwien.auto.calimero.dptxlator.DPTXlatorDate; +import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime; +import tuwien.auto.calimero.dptxlator.DPTXlatorTime; +import tuwien.auto.calimero.dptxlator.TranslatorTypes; + +/** + * This class encodes openHAB data types to strings for sending via Calimero + * + * Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ValueEncoder { + private static final Logger LOGGER = LoggerFactory.getLogger(ValueEncoder.class); + + private ValueEncoder() { + // prevent instantiation + } + + /** + * Formats the given value as String for outputting via Calimero. + * + * @param value the value + * @param dptId the DPT id to use for formatting the string (e.g. 9.001) + * @return the value formatted as String + */ + public static @Nullable String encode(Type value, String dptId) { + Matcher m = DPTUtil.DPT_PATTERN.matcher(dptId); + if (!m.matches() || m.groupCount() != 2) { + LOGGER.warn("Couldn't identify main/sub number in dptId '{}'", dptId); + return null; + } + + String mainNumber = m.group("main"); + + try { + DPTXlator translator = TranslatorTypes.createTranslator(Integer.parseInt(mainNumber), + NORMALIZED_DPT.getOrDefault(dptId, dptId)); + DPT dpt = translator.getType(); + + // check for HSBType first, because it extends PercentType as well + if (value instanceof HSBType) { + return handleHSBType(dptId, (HSBType) value); + } else if (value instanceof OnOffType) { + return OnOffType.OFF.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue(); + } else if (value instanceof UpDownType) { + return UpDownType.UP.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue(); + } else if (value instanceof IncreaseDecreaseType) { + DPT valueDPT = ((DPTXlator3BitControlled.DPT3BitControlled) dpt).getControlDPT(); + return IncreaseDecreaseType.DECREASE.equals(value) ? valueDPT.getLowerValue() + " 5" + : valueDPT.getUpperValue() + " 5"; + } else if (value instanceof OpenClosedType) { + return OpenClosedType.CLOSED.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue(); + } else if (value instanceof StopMoveType) { + return StopMoveType.STOP.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue(); + } else if (value instanceof PercentType) { + int intValue = ((PercentType) value).intValue(); + return "251.600".equals(dptId) ? String.format("- - - %d %%", intValue) : String.valueOf(intValue); + } else if (value instanceof DecimalType || value instanceof QuantityType) { + return handleNumericTypes(dptId, mainNumber, dpt, value); + } else if (value instanceof StringType) { + return value.toString(); + } else if (value instanceof DateTimeType) { + return handleDateTimeType(dptId, (DateTimeType) value); + } + } catch (KNXException e) { + return null; + } catch (Exception e) { + LOGGER.warn("An exception occurred converting value {} to dpt id {}: error message={}", value, dptId, + e.getMessage()); + return null; + } + + LOGGER.debug("formatAsDPTString: Couldn't convert value {} to dpt id {} (no mapping).", value, dptId); + return null; + } + + /** + * Formats the given internal dateType to a knx readable String + * according to the target datapoint type dpt. + * + * @param value the input value + * @param dptId the target datapoint type + * + * @return a String which contains either an ISO8601 formatted date (yyyy-mm-dd), + * a formatted 24-hour clock with the day of week prepended (Mon, 12:00:00) or + * a formatted 24-hour clock (12:00:00) + */ + private static @Nullable String handleDateTimeType(String dptId, DateTimeType value) { + if (DPTXlatorDate.DPT_DATE.getID().equals(dptId)) { + return value.format("%tF"); + } else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dptId)) { + return value.format(Locale.US, "%1$ta, %1$tT"); + } else if (DPTXlatorDateTime.DPT_DATE_TIME.getID().equals(dptId)) { + return value.format(Locale.US, "%tF %1$tT"); + } + LOGGER.warn("Could not format DateTimeType for datapoint type '{}'", dptId); + return null; + } + + private static String handleHSBType(String dptId, HSBType hsb) { + switch (dptId) { + case "232.600": + return "r:" + convertPercentToByte(hsb.getRed()) + " g:" + convertPercentToByte(hsb.getGreen()) + " b:" + + convertPercentToByte(hsb.getBlue()); + case "232.60000": + // MDT specific: mis-use 232.600 for hsv instead of rgb + int hue = hsb.getHue().toBigDecimal().multiply(BigDecimal.valueOf(255)) + .divide(BigDecimal.valueOf(360), 2, RoundingMode.HALF_UP).intValue(); + return "r:" + hue + " g:" + convertPercentToByte(hsb.getSaturation()) + " b:" + + convertPercentToByte(hsb.getBrightness()); + case "242.600": + double[] xyY = ColorUtil.hsbToXY(hsb); + return String.format("(%,.4f %,.4f) %,.1f %%", xyY[0], xyY[1], xyY[2] * 100.0); + case "251.600": + return String.format("%d %d %d - %%", hsb.getRed().intValue(), hsb.getGreen().intValue(), + hsb.getBlue().intValue()); + case "5.003": + return hsb.getHue().toString(); + default: + return hsb.getBrightness().toString(); + } + } + + private static String handleNumericTypes(String dptId, String mainNumber, DPT dpt, Type value) { + BigDecimal bigDecimal; + if (value instanceof DecimalType decimalType) { + bigDecimal = decimalType.toBigDecimal(); + } else { + String unit = DPTUnits.getUnitForDpt(dptId); + + // exception for DPT using temperature differences + // - conversion °C or °F to K is wrong for differences, + // - stick to the unit given, fix the scaling for °F + // 9.002 DPT_Value_Tempd + // 9.003 DPT_Value_Tempa + // 9.023 DPT_KelvinPerPercent + if (DPTXlator2ByteFloat.DPT_TEMPERATURE_DIFFERENCE.getID().equals(dptId) + || DPTXlator2ByteFloat.DPT_TEMPERATURE_GRADIENT.getID().equals(dptId) + || DPTXlator2ByteFloat.DPT_KELVIN_PER_PERCENT.getID().equals(dptId)) { + // match unicode character or °C + if (value.toString().contains(SIUnits.CELSIUS.getSymbol()) || value.toString().contains("°C")) { + unit = unit.replace("K", "°C"); + } else if (value.toString().contains("°F")) { + unit = unit.replace("K", "°F"); + value = ((QuantityType) value).multiply(BigDecimal.valueOf(5.0 / 9.0)); + } + } else if (DPTXlator4ByteFloat.DPT_LIGHT_QUANTITY.getID().equals(dptId)) { + if (!value.toString().contains("J")) { + unit = unit.replace("J", "lm*s"); + } + } else if (DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID().equals(dptId)) { + // use alternate definition of flux + if (value.toString().contains("C")) { + unit = "C"; + } + } + + if (unit != null) { + QuantityType converted = ((QuantityType) value).toUnit(unit); + if (converted == null) { + LOGGER.warn("Could not convert {} to unit {}, stripping unit only. Check your configuration.", + value, unit); + bigDecimal = ((QuantityType) value).toBigDecimal(); + } else { + bigDecimal = converted.toBigDecimal(); + } + } else { + bigDecimal = ((QuantityType) value).toBigDecimal(); + } + } + switch (mainNumber) { + case "2": + DPT valueDPT = ((DPTXlator1BitControlled.DPT1BitControlled) dpt).getValueDPT(); + switch (bigDecimal.intValue()) { + case 0: + return "0 " + valueDPT.getLowerValue(); + case 1: + return "0 " + valueDPT.getUpperValue(); + case 2: + return "1 " + valueDPT.getLowerValue(); + default: + return "1 " + valueDPT.getUpperValue(); + } + case "18": + int intVal = bigDecimal.intValue(); + if (intVal > 63) { + return "learn " + (intVal - 0x80); + } else { + return "activate " + intVal; + } + default: + return bigDecimal.stripTrailingZeros().toPlainString(); + } + } + + /** + * convert 0...100% to 1 byte 0..255 + * + * @param percent + * @return int 0..255 + */ + private static int convertPercentToByte(PercentType percent) { + return percent.toBigDecimal().multiply(BigDecimal.valueOf(255)) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).intValue(); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java index f0759359641..b5265982c99 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java @@ -15,6 +15,7 @@ package org.openhab.binding.knx.internal.factory; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; import java.util.Collection; +import java.util.Map; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -38,6 +39,7 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; /** @@ -54,17 +56,23 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory { THING_TYPE_IP_BRIDGE, THING_TYPE_SERIAL_BRIDGE); @Nullable - private NetworkAddressService networkAddressService; + private final NetworkAddressService networkAddressService; private final SerialPortManager serialPortManager; @Activate - public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, + public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map config, final @Reference TranslationProvider translationProvider, final @Reference LocaleProvider localeProvider, final @Reference SerialPortManager serialPortManager) { KNXTranslationProvider.I18N.setProvider(localeProvider, translationProvider); this.networkAddressService = networkAddressService; this.serialPortManager = serialPortManager; SerialTransportAdapter.setSerialPortManager(serialPortManager); + modified(config); + } + + @Modified + protected void modified(Map config) { + disableUoM = (boolean) config.getOrDefault(CONFIG_DISABLE_UOM, false); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java deleted file mode 100644 index fd181d44c47..00000000000 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.handler; - -import java.util.Map; -import java.util.Random; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.knx.internal.client.DeviceInspector; -import org.openhab.binding.knx.internal.client.DeviceInspector.Result; -import org.openhab.binding.knx.internal.client.KNXClient; -import org.openhab.binding.knx.internal.config.DeviceConfig; -import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.ThingStatusInfo; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import tuwien.auto.calimero.IndividualAddress; -import tuwien.auto.calimero.KNXException; -import tuwien.auto.calimero.KNXFormatException; - -/** - * Base class for KNX thing handlers. - * - * @author Simon Kaufmann - initial contribution and API - * - */ -@NonNullByDefault -public abstract class AbstractKNXThingHandler extends BaseThingHandler implements GroupAddressListener { - - private static final int INITIAL_PING_DELAY = 5; - private final Logger logger = LoggerFactory.getLogger(AbstractKNXThingHandler.class); - - protected @Nullable IndividualAddress address; - private @Nullable ScheduledFuture descriptionJob; - private boolean filledDescription = false; - private final Random random = new Random(); - - private @Nullable ScheduledFuture pollingJob; - - public AbstractKNXThingHandler(Thing thing) { - super(thing); - } - - protected final ScheduledExecutorService getScheduler() { - return getBridgeHandler().getScheduler(); - } - - protected final ScheduledExecutorService getBackgroundScheduler() { - return getBridgeHandler().getBackgroundScheduler(); - } - - protected final KNXBridgeBaseThingHandler getBridgeHandler() { - Bridge bridge = getBridge(); - if (bridge != null) { - KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler(); - if (handler != null) { - return handler; - } - } - throw new IllegalStateException("The bridge must not be null and must be initialized"); - } - - protected final KNXClient getClient() { - return getBridgeHandler().getClient(); - } - - protected final boolean describeDevice(@Nullable IndividualAddress address) { - if (address == null) { - return false; - } - DeviceInspector inspector = new DeviceInspector(getClient().getDeviceInfoClient(), address); - Result result = inspector.readDeviceInfo(); - if (result != null) { - Map properties = editProperties(); - properties.putAll(result.getProperties()); - updateProperties(properties); - return true; - } - return false; - } - - protected final String asduToHex(byte[] asdu) { - final char[] hexCode = "0123456789ABCDEF".toCharArray(); - StringBuilder sb = new StringBuilder(2 + asdu.length * 2); - sb.append("0x"); - for (byte b : asdu) { - sb.append(hexCode[(b >> 4) & 0xF]); - sb.append(hexCode[(b & 0xF)]); - } - return sb.toString(); - } - - protected final void restart() { - if (address != null) { - getClient().restartNetworkDevice(address); - } - } - - @Override - public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { - if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { - attachToClient(); - } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { - detachFromClient(); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); - } - } - - @Override - public void initialize() { - attachToClient(); - } - - @Override - public void dispose() { - detachFromClient(); - } - - protected abstract void scheduleReadJobs(); - - protected abstract void cancelReadFutures(); - - private void pollDeviceStatus() { - try { - if (address != null && getClient().isConnected()) { - logger.debug("Polling individual address '{}'", address); - boolean isReachable = getClient().isReachable(address); - if (isReachable) { - updateStatus(ThingStatus.ONLINE); - DeviceConfig config = getConfigAs(DeviceConfig.class); - if (!filledDescription && config.getFetch()) { - Future descriptionJob = this.descriptionJob; - if (descriptionJob == null || descriptionJob.isCancelled()) { - long initialDelay = Math.round(config.getPingInterval() * random.nextFloat()); - this.descriptionJob = getBackgroundScheduler().schedule(() -> { - filledDescription = describeDevice(address); - }, initialDelay, TimeUnit.SECONDS); - } - } - } else { - updateStatus(ThingStatus.OFFLINE); - } - } - } catch (KNXException e) { - logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(), - e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - KNXTranslationProvider.I18N.getLocalizedException(e)); - } - } - - protected void attachToClient() { - if (!getClient().isConnected()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); - return; - } - DeviceConfig config = getConfigAs(DeviceConfig.class); - try { - if (!config.getAddress().isEmpty()) { - updateStatus(ThingStatus.UNKNOWN); - address = new IndividualAddress(config.getAddress()); - - long pingInterval = config.getPingInterval(); - long initialPingDelay = Math.round(INITIAL_PING_DELAY * random.nextFloat()); - - ScheduledFuture pollingJob = this.pollingJob; - if ((pollingJob == null || pollingJob.isCancelled())) { - logger.debug("'{}' will be polled every {}s", getThing().getUID(), pingInterval); - this.pollingJob = getBackgroundScheduler().scheduleWithFixedDelay(() -> pollDeviceStatus(), - initialPingDelay, pingInterval, TimeUnit.SECONDS); - } - } else { - updateStatus(ThingStatus.ONLINE); - } - } catch (KNXFormatException e) { - logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(), - e.getMessage()); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - KNXTranslationProvider.I18N.getLocalizedException(e)); - } - getClient().registerGroupAddressListener(this); - scheduleReadJobs(); - } - - protected void detachFromClient() { - final var pollingJobSynced = pollingJob; - if (pollingJobSynced != null) { - pollingJobSynced.cancel(true); - pollingJob = null; - } - final var descriptionJobSynced = descriptionJob; - if (descriptionJobSynced != null) { - descriptionJobSynced.cancel(true); - descriptionJob = null; - } - cancelReadFutures(); - Bridge bridge = getBridge(); - if (bridge != null) { - KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler(); - if (handler != null) { - handler.getClient().unregisterGroupAddressListener(this); - } - } - } -} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java index aee86534ab4..8fa423cdcee 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java @@ -15,37 +15,48 @@ package org.openhab.binding.knx.internal.handler; import static org.openhab.binding.knx.internal.KNXBindingConstants.*; import java.math.BigDecimal; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; +import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.KNXBindingConstants; -import org.openhab.binding.knx.internal.KNXTypeMapper; -import org.openhab.binding.knx.internal.channel.KNXChannelType; -import org.openhab.binding.knx.internal.channel.KNXChannelTypes; +import org.openhab.binding.knx.internal.channel.KNXChannel; +import org.openhab.binding.knx.internal.channel.KNXChannelFactory; import org.openhab.binding.knx.internal.client.AbstractKNXClient; +import org.openhab.binding.knx.internal.client.DeviceInspector; import org.openhab.binding.knx.internal.client.InboundSpec; +import org.openhab.binding.knx.internal.client.KNXClient; import org.openhab.binding.knx.internal.client.OutboundSpec; import org.openhab.binding.knx.internal.config.DeviceConfig; -import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper; -import org.openhab.core.config.core.Configuration; +import org.openhab.binding.knx.internal.dpt.DPTUtil; +import org.openhab.binding.knx.internal.dpt.ValueDecoder; +import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; +import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; -import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.Type; import org.openhab.core.types.UnDefType; +import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,19 +72,26 @@ import tuwien.auto.calimero.datapoint.Datapoint; * bus and updating the channels correspondingly. * * @author Simon Kaufmann - Initial contribution and API + * @author Jan N. Klug - Refactored for performance */ @NonNullByDefault -public class DeviceThingHandler extends AbstractKNXThingHandler { - +public class DeviceThingHandler extends BaseThingHandler implements GroupAddressListener { + private static final int INITIAL_PING_DELAY = 5; private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class); - private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper(); private final Set groupAddresses = ConcurrentHashMap.newKeySet(); - private final Set groupAddressesWriteBlockedOnce = ConcurrentHashMap.newKeySet(); - private final Set groupAddressesRespondingSpec = ConcurrentHashMap.newKeySet(); + private final ExpiringCacheMap groupAddressesWriteBlocked = new ExpiringCacheMap<>( + Duration.ofMillis(1000)); + private final Map groupAddressesRespondingSpec = new ConcurrentHashMap<>(); private final Map> readFutures = new ConcurrentHashMap<>(); private final Map> channelFutures = new ConcurrentHashMap<>(); + private final Map knxChannels = new ConcurrentHashMap<>(); + private final Random random = new Random(); + protected @Nullable IndividualAddress address; private int readInterval; + private @Nullable ScheduledFuture descriptionJob; + private boolean filledDescription = false; + private @Nullable ScheduledFuture pollingJob; public DeviceThingHandler(Thing thing) { super(thing); @@ -81,43 +99,34 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { @Override public void initialize() { - super.initialize(); + attachToClient(); DeviceConfig config = getConfigAs(DeviceConfig.class); readInterval = config.getReadInterval(); - initializeGroupAddresses(); - } - - private void initializeGroupAddresses() { - forAllChannels((selector, channelConfiguration) -> { - groupAddresses.addAll(selector.getReadAddresses(channelConfiguration)); - groupAddresses.addAll(selector.getWriteAddresses(channelConfiguration)); - groupAddresses.addAll(selector.getListenAddresses(channelConfiguration)); + // gather all GAs from channel configurations and create channels + getThing().getChannels().forEach(channel -> { + KNXChannel knxChannel = KNXChannelFactory.createKnxChannel(channel); + knxChannels.put(channel.getUID(), knxChannel); + groupAddresses.addAll(knxChannel.getAllGroupAddresses()); }); } @Override public void dispose() { - cancelChannelFutures(); - freeGroupAddresses(); - super.dispose(); - } - - private void cancelChannelFutures() { for (ChannelUID channelUID : channelFutures.keySet()) { channelFutures.computeIfPresent(channelUID, (k, v) -> { v.cancel(true); return null; }); } - } - private void freeGroupAddresses() { groupAddresses.clear(); - groupAddressesWriteBlockedOnce.clear(); + groupAddressesWriteBlocked.clear(); groupAddressesRespondingSpec.clear(); + knxChannels.clear(); + + detachFromClient(); } - @Override protected void cancelReadFutures() { for (GroupAddress groupAddress : readFutures.keySet()) { readFutures.computeIfPresent(groupAddress, (k, v) -> { @@ -127,62 +136,31 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { } } - @FunctionalInterface - private interface ChannelFunction { - void apply(KNXChannelType channelType, Configuration configuration) throws KNXException; - } - - private void withKNXType(ChannelUID channelUID, ChannelFunction function) { - Channel channel = getThing().getChannel(channelUID.getId()); - if (channel == null) { - logger.warn("Channel '{}' does not exist", channelUID); - return; - } - withKNXType(channel, function); - } - - private void withKNXType(Channel channel, ChannelFunction function) { - try { - KNXChannelType selector = getKNXChannelType(channel); - function.apply(selector, channel.getConfiguration()); - } catch (KNXException e) { - logger.warn("An error occurred on channel {}: {}", channel.getUID(), e.getMessage(), e); - } - } - - private void forAllChannels(ChannelFunction function) { - for (Channel channel : getThing().getChannels()) { - withKNXType(channel, function); - } - } - @Override public void channelLinked(ChannelUID channelUID) { - if (!isControl(channelUID)) { - withKNXType(channelUID, (selector, configuration) -> { - scheduleRead(selector, configuration); - }); + KNXChannel knxChannel = knxChannels.get(channelUID); + if (knxChannel == null) { + logger.warn("Channel '{}' received a channel linked event, but no KNXChannel found", channelUID); + return; + } + if (!knxChannel.isControl()) { + scheduleRead(knxChannel); } } - @Override protected void scheduleReadJobs() { cancelReadFutures(); - for (Channel channel : getThing().getChannels()) { - if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) { - withKNXType(channel, (selector, configuration) -> { - scheduleRead(selector, configuration); - }); + for (KNXChannel knxChannel : knxChannels.values()) { + if (isLinked(knxChannel.getChannelUID()) && !knxChannel.isControl()) { + scheduleRead(knxChannel); } } } - private void scheduleRead(KNXChannelType selector, Configuration configuration) throws KNXFormatException { - List readSpecs = selector.getReadSpec(configuration); + private void scheduleRead(KNXChannel knxChannel) { + List readSpecs = knxChannel.getReadSpec(); for (InboundSpec readSpec : readSpecs) { - for (GroupAddress groupAddress : readSpec.getGroupAddresses()) { - scheduleReadJob(groupAddress, readSpec.getDPT()); - } + readSpec.getGroupAddresses().forEach(ga -> scheduleReadJob(ga, readSpec.getDPT())); } } @@ -201,7 +179,7 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { private void readDatapoint(GroupAddress groupAddress, String dpt) { if (getClient().isConnected()) { - if (!isDPTSupported(dpt)) { + if (DPTUtil.getAllowedTypes(dpt).isEmpty()) { logger.warn("DPT '{}' is not supported by the KNX binding", dpt); return; } @@ -215,89 +193,71 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { return groupAddresses.contains(destination); } - /** KNXIO remember controls, removeIf may be null */ - @SuppressWarnings("null") - private void rememberRespondingSpec(OutboundSpec commandSpec, boolean add) { - GroupAddress ga = commandSpec.getGroupAddress(); - if (ga != null) { - groupAddressesRespondingSpec.removeIf(spec -> spec.getGroupAddress().equals(ga)); - } - if (add) { - groupAddressesRespondingSpec.add(commandSpec); - } - logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga, - groupAddressesRespondingSpec.size(), add); - } - /** Handling commands triggered from openHAB */ @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.trace("Handling command '{}' for channel '{}'", command, channelUID); - if (command instanceof RefreshType && !isControl(channelUID)) { + KNXChannel knxChannel = knxChannels.get(channelUID); + if (knxChannel == null) { + logger.warn("Channel '{}' received command, but no KNXChannel found", channelUID); + return; + } + if (command instanceof RefreshType && !knxChannel.isControl()) { logger.debug("Refreshing channel '{}'", channelUID); - withKNXType(channelUID, (selector, configuration) -> { - scheduleRead(selector, configuration); - }); + scheduleRead(knxChannel); } else { - switch (channelUID.getId()) { - case CHANNEL_RESET: - if (address != null) { - restart(); - } - break; - default: - withKNXType(channelUID, (selector, channelConfiguration) -> { - OutboundSpec commandSpec = selector.getCommandSpec(channelConfiguration, typeHelper, command); - // only send GroupValueWrite to KNX if GA is not blocked once - if (commandSpec != null - && !groupAddressesWriteBlockedOnce.remove(commandSpec.getGroupAddress())) { - getClient().writeToKNX(commandSpec); - if (isControl(channelUID)) { - rememberRespondingSpec(commandSpec, true); - } - } else { - logger.debug( - "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'", - channelUID, command, command.getClass().getSimpleName()); + if (CHANNEL_RESET.equals(channelUID.getId())) { + if (address != null) { + restart(); + } + } else { + try { + OutboundSpec commandSpec = knxChannel.getCommandSpec(command); + // only send GroupValueWrite to KNX if GA is not blocked once + if (commandSpec != null) { + GroupAddress destination = commandSpec.getGroupAddress(); + if (knxChannel.isControl()) { + // always remember, otherwise we might send an old state + groupAddressesRespondingSpec.put(destination, commandSpec); } - }); - break; + if (groupAddressesWriteBlocked.get(destination) != null) { + logger.debug("Write to {} blocked for 1s/one call after read.", destination); + groupAddressesWriteBlocked.invalidate(destination); + } else { + getClient().writeToKNX(commandSpec); + } + } else { + logger.debug( + "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'", + channelUID, command, command.getClass().getSimpleName()); + } + } catch (KNXException e) { + logger.warn("An error occurred while handling command '{}' on channel '{}': {}", command, + channelUID, e.getMessage()); + } } } } - private boolean isControl(ChannelUID channelUID) { - ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID); - return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId()); - } - - private ChannelTypeUID getChannelTypeUID(ChannelUID channelUID) { - Channel channel = getThing().getChannel(channelUID.getId()); - Objects.requireNonNull(channel); - ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); - Objects.requireNonNull(channelTypeUID); - return channelTypeUID; - } - /** KNXIO */ - private void sendGroupValueResponse(Channel channel, GroupAddress destination) { - Set rsa = getKNXChannelType(channel).getWriteAddresses(channel.getConfiguration()); + private void sendGroupValueResponse(ChannelUID channelUID, GroupAddress destination) { + KNXChannel knxChannel = knxChannels.get(channelUID); + if (knxChannel == null) { + return; + } + Set rsa = knxChannel.getWriteAddresses(); if (!rsa.isEmpty()) { logger.trace("onGroupRead size '{}'", rsa.size()); - withKNXType(channel, (selector, configuration) -> { - Optional os = groupAddressesRespondingSpec.stream().filter(spec -> { - GroupAddress groupAddress = spec.getGroupAddress(); - if (groupAddress != null) { - return groupAddress.equals(destination); - } - return false; - }).findFirst(); - if (os.isPresent()) { - logger.trace("onGroupRead respondToKNX '{}'", os.get().getGroupAddress()); - /** KNXIO: sending real "GroupValueResponse" to the KNX bus. */ - getClient().respondToKNX(os.get()); + OutboundSpec os = groupAddressesRespondingSpec.get(destination); + if (os != null) { + logger.trace("onGroupRead respondToKNX '{}'", + os.getGroupAddress()); /* KNXIO: sending real "GroupValueResponse" to the KNX bus. */ + try { + getClient().respondToKNX(os); + } catch (KNXException e) { + logger.warn("An error occurred on channel {}: {}", channelUID, e.getMessage(), e); } - }); + } } } @@ -308,22 +268,25 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu) { logger.trace("onGroupRead Thing '{}' received a GroupValueRead telegram from '{}' for destination '{}'", getThing().getUID(), source, destination); - for (Channel channel : getThing().getChannels()) { - if (isControl(channel.getUID())) { - withKNXType(channel, (selector, configuration) -> { - OutboundSpec responseSpec = selector.getResponseSpec(configuration, destination, - RefreshType.REFRESH); - if (responseSpec != null) { - logger.trace("onGroupRead isControl -> postCommand"); - // This event should be sent to KNX as GroupValueResponse immediately. - sendGroupValueResponse(channel, destination); - // Send REFRESH to openHAB to get this event for scripting with postCommand - // and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after - // postCommand is done! - groupAddressesWriteBlockedOnce.add(destination); - postCommand(channel.getUID().getId(), RefreshType.REFRESH); + for (KNXChannel knxChannel : knxChannels.values()) { + if (knxChannel.isControl()) { + OutboundSpec responseSpec = knxChannel.getResponseSpec(destination, RefreshType.REFRESH); + if (responseSpec != null) { + logger.trace("onGroupRead isControl -> postCommand"); + // This event should be sent to KNX as GroupValueResponse immediately. + sendGroupValueResponse(knxChannel.getChannelUID(), destination); + + // block write attempts for 1s or 1 request to prevent loops + if (!groupAddressesWriteBlocked.containsKey(destination)) { + groupAddressesWriteBlocked.put(destination, () -> null); } - }); + groupAddressesWriteBlocked.putValue(destination, true); + + // Send REFRESH to openHAB to get this event for scripting with postCommand + // and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after + // postCommand is done! + postCommand(knxChannel.getChannelUID(), RefreshType.REFRESH); + } } } } @@ -346,91 +309,219 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'", getThing().getUID(), source, destination); - for (Channel channel : getThing().getChannels()) { - withKNXType(channel, (selector, configuration) -> { - InboundSpec listenSpec = selector.getListenSpec(configuration, destination); - if (listenSpec != null) { - logger.trace( - "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'", - getThing().getUID(), destination, channel.getUID()); - /** - * Remember current KNXIO outboundSpec only if it is a control channel. - */ - if (isControl(channel.getUID())) { - logger.trace("onGroupWrite isControl"); - Type type = typeHelper.toType( - new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT()), - asdu); - if (type != null) { - OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type); - if (commandSpec != null) { - rememberRespondingSpec(commandSpec, true); - } + for (KNXChannel knxChannel : knxChannels.values()) { + InboundSpec listenSpec = knxChannel.getListenSpec(destination); + if (listenSpec != null) { + logger.trace( + "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'", + getThing().getUID(), destination, knxChannel.getChannelUID()); + /** + * Remember current KNXIO outboundSpec only if it is a control channel. + */ + if (knxChannel.isControl()) { + logger.trace("onGroupWrite isControl"); + Type value = ValueDecoder.decode(listenSpec.getDPT(), asdu, knxChannel.preferredType()); + if (value != null) { + OutboundSpec commandSpec = knxChannel.getCommandSpec(value); + if (commandSpec != null) { + groupAddressesRespondingSpec.put(destination, commandSpec); } } - processDataReceived(destination, asdu, listenSpec, channel.getUID()); } - }); + processDataReceived(destination, asdu, listenSpec, knxChannel); + } } } private void processDataReceived(GroupAddress destination, byte[] asdu, InboundSpec listenSpec, - ChannelUID channelUID) { - if (!isDPTSupported(listenSpec.getDPT())) { + KNXChannel knxChannel) { + if (DPTUtil.getAllowedTypes(listenSpec.getDPT()).isEmpty()) { logger.warn("DPT '{}' is not supported by the KNX binding.", listenSpec.getDPT()); return; } - Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT()); - Type type = typeHelper.toType(datapoint, asdu); - - if (type != null) { - if (isControl(channelUID)) { - Channel channel = getThing().getChannel(channelUID.getId()); - Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY) - : null; - int frequency = repeat != null ? ((BigDecimal) repeat).intValue() : 0; - if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(getChannelTypeUID(channelUID).getId()) - && (type instanceof UnDefType || type instanceof IncreaseDecreaseType) && frequency > 0) { + Type value = ValueDecoder.decode(listenSpec.getDPT(), asdu, knxChannel.preferredType()); + if (value != null) { + if (knxChannel.isControl()) { + ChannelUID channelUID = knxChannel.getChannelUID(); + int frequency; + if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(knxChannel.getChannelType())) { + // if we have a dimmer control channel, check if a frequency is defined + Channel channel = getThing().getChannel(channelUID); + if (channel == null) { + logger.warn("Failed to find channel for ChannelUID '{}'", channelUID); + return; + } + frequency = ((BigDecimal) Objects.requireNonNullElse( + channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY), BigDecimal.ZERO)) + .intValue(); + } else { + // disable dimming by binding + frequency = 0; + } + if ((value instanceof UnDefType || value instanceof IncreaseDecreaseType) && frequency > 0) { // continuous dimming by the binding - if (UnDefType.UNDEF.equals(type)) { - channelFutures.computeIfPresent(channelUID, (k, v) -> { - v.cancel(false); - return null; - }); - } else if (type instanceof IncreaseDecreaseType) { - channelFutures.compute(channelUID, (k, v) -> { - if (v != null) { - v.cancel(true); - } - return scheduler.scheduleWithFixedDelay(() -> postCommand(channelUID, (Command) type), 0, - frequency, TimeUnit.MILLISECONDS); - }); + // cancel a running scheduler before adding a new (and only add if not UnDefType) + ScheduledFuture oldFuture = channelFutures.remove(channelUID); + if (oldFuture != null) { + oldFuture.cancel(true); + } + if (value instanceof IncreaseDecreaseType) { + channelFutures.put(channelUID, scheduler.scheduleWithFixedDelay( + () -> postCommand(channelUID, (Command) value), 0, frequency, TimeUnit.MILLISECONDS)); } } else { - if (type instanceof Command) { + if (value instanceof Command command) { logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address); - postCommand(channelUID, (Command) type); + postCommand(channelUID, command); } } } else { - if (type instanceof State && !(type instanceof UnDefType)) { - updateState(channelUID, (State) type); + if (value instanceof State state && !(value instanceof UnDefType)) { + updateState(knxChannel.getChannelUID(), state); } } } else { - String s = asduToHex(asdu); logger.warn( - "Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')", - destination, datapoint, s); + "Ignoring KNX bus data for channel '{}': couldn't transform to any Type (GA='{}', DPT='{}', data='{}')", + knxChannel.getChannelUID(), destination, listenSpec.getDPT(), HexUtils.bytesToHex(asdu)); } } - private boolean isDPTSupported(@Nullable String dpt) { - return typeHelper.toTypeClass(dpt) != null; + protected final ScheduledExecutorService getScheduler() { + return getBridgeHandler().getScheduler(); } - private KNXChannelType getKNXChannelType(Channel channel) { - return KNXChannelTypes.getType(channel.getChannelTypeUID()); + protected final ScheduledExecutorService getBackgroundScheduler() { + return getBridgeHandler().getBackgroundScheduler(); + } + + protected final KNXBridgeBaseThingHandler getBridgeHandler() { + Bridge bridge = getBridge(); + if (bridge != null) { + KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler(); + if (handler != null) { + return handler; + } + } + throw new IllegalStateException("The bridge must not be null and must be initialized"); + } + + protected final KNXClient getClient() { + return getBridgeHandler().getClient(); + } + + protected final boolean describeDevice(@Nullable IndividualAddress address) { + if (address == null) { + return false; + } + DeviceInspector inspector = new DeviceInspector(getClient().getDeviceInfoClient(), address); + DeviceInspector.Result result = inspector.readDeviceInfo(); + if (result != null) { + Map properties = editProperties(); + properties.putAll(result.getProperties()); + updateProperties(properties); + return true; + } + return false; + } + + protected final void restart() { + if (address != null) { + getClient().restartNetworkDevice(address); + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + attachToClient(); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + detachFromClient(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + private void pollDeviceStatus() { + try { + if (address != null && getClient().isConnected()) { + logger.debug("Polling individual address '{}'", address); + boolean isReachable = getClient().isReachable(address); + if (isReachable) { + updateStatus(ThingStatus.ONLINE); + DeviceConfig config = getConfigAs(DeviceConfig.class); + if (!filledDescription && config.getFetch()) { + Future descriptionJob = this.descriptionJob; + if (descriptionJob == null || descriptionJob.isCancelled()) { + long initialDelay = Math.round(config.getPingInterval() * random.nextFloat()); + this.descriptionJob = getBackgroundScheduler().schedule(() -> { + filledDescription = describeDevice(address); + }, initialDelay, TimeUnit.SECONDS); + } + } + } else { + updateStatus(ThingStatus.OFFLINE); + } + } + } catch (KNXException e) { + logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(), + e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + KNXTranslationProvider.I18N.getLocalizedException(e)); + } + } + + protected void attachToClient() { + if (!getClient().isConnected()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + return; + } + DeviceConfig config = getConfigAs(DeviceConfig.class); + try { + if (!config.getAddress().isEmpty()) { + updateStatus(ThingStatus.UNKNOWN); + address = new IndividualAddress(config.getAddress()); + + long pingInterval = config.getPingInterval(); + long initialPingDelay = Math.round(INITIAL_PING_DELAY * random.nextFloat()); + + ScheduledFuture pollingJob = this.pollingJob; + if ((pollingJob == null || pollingJob.isCancelled())) { + logger.debug("'{}' will be polled every {}s", getThing().getUID(), pingInterval); + this.pollingJob = getBackgroundScheduler().scheduleWithFixedDelay(this::pollDeviceStatus, + initialPingDelay, pingInterval, TimeUnit.SECONDS); + } + } else { + updateStatus(ThingStatus.ONLINE); + } + } catch (KNXFormatException e) { + logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(), + e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + KNXTranslationProvider.I18N.getLocalizedException(e)); + } + getClient().registerGroupAddressListener(this); + scheduleReadJobs(); + } + + protected void detachFromClient() { + final var pollingJobSynced = pollingJob; + if (pollingJobSynced != null) { + pollingJobSynced.cancel(true); + pollingJob = null; + } + final var descriptionJobSynced = descriptionJob; + if (descriptionJobSynced != null) { + descriptionJobSynced.cancel(true); + descriptionJob = null; + } + cancelReadFutures(); + Bridge bridge = getBridge(); + if (bridge != null) { + KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler(); + if (handler != null) { + handler.getClient().unregisterGroupAddressListener(this); + } + } } } diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/addon/addon.xml index f403aff9753..e8684e910bd 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/addon/addon.xml @@ -7,4 +7,12 @@ KNX Binding This binding supports connecting to a KNX bus + + + false + + This disables Units of Measurement support for incoming values. + + + diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelFactoryTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelFactoryTest.java new file mode 100644 index 00000000000..78aa0586de0 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelFactoryTest.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.channel; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.openhab.binding.knx.internal.KNXBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * + * @author Holger Friedrich - Initial Contribution + * + */ +@NonNullByDefault +class KNXChannelFactoryTest { + + /** + * This test checks if channels with invalid channelTypeUID lead to the intended exception. + * Side effect is testing if KNXChannelFactory can be instantiated (this is not the case e.g. when types with + * duplicate channel types are created) + */ + @Test + public void testNullChannelUidFails() { + Channel channel = mock(Channel.class); + + assertThrows(IllegalArgumentException.class, () -> { + KNXChannelFactory.createKnxChannel(channel); + }); + } + + @Test + public void testInvalidChannelUidFails() { + Channel channel = mock(Channel.class); + when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("a:b:c")); + + assertThrows(IllegalArgumentException.class, () -> { + KNXChannelFactory.createKnxChannel(channel); + }); + } + + @ParameterizedTest + @ValueSource(strings = { CHANNEL_COLOR, CHANNEL_COLOR_CONTROL, CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL, + CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL, CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL, CHANNEL_NUMBER, + CHANNEL_NUMBER_CONTROL, CHANNEL_ROLLERSHUTTER, CHANNEL_ROLLERSHUTTER_CONTROL, CHANNEL_STRING, + CHANNEL_STRING_CONTROL, CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL }) + public void testSuccess(String channeltype) { + Channel channel = mock(Channel.class); + Configuration configuration = new Configuration( + Map.of("key1", "5.001:<1/2/3+4/5/6+1/5/6", "key2", "1.001:7/1/9+1/1/2")); + when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("knx:" + channeltype)); + when(channel.getConfiguration()).thenReturn(configuration); + when(channel.getAcceptedItemType()).thenReturn("none"); + + assertNotNull(KNXChannelFactory.createKnxChannel(channel)); + } +} diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTest.java new file mode 100644 index 00000000000..866a1410f1c --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTest.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.channel; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.UnDefType; + +import tuwien.auto.calimero.GroupAddress; +import tuwien.auto.calimero.KNXFormatException; + +/** + * + * @author Simon Kaufmann - Initial contribution + * + */ +@NonNullByDefault +class KNXChannelTest { + + @Test + public void invalidFails() { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<1/3/22+0/3/22+<0/8/15"); + assertNull(res); + } + + @Test + void testParseWithDptMultipleWithRead() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<1/3/22+0/3/22+<0/7/15"); + + if (res == null) { + fail(); + return; + } + + assertEquals("5.001", res.getDPT()); + assertEquals(new GroupAddress("1/3/22"), res.getMainGA()); + assertTrue(res.getReadGAs().contains(res.getMainGA())); + assertEquals(3, res.getListenGAs().size()); + assertEquals(2, res.getReadGAs().size()); + } + + @Test + void testParseWithDptMultipleWithoutRead() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:1/3/22+0/3/22+0/7/15"); + + if (res == null) { + fail(); + return; + } + + assertEquals("5.001", res.getDPT()); + assertEquals(new GroupAddress("1/3/22"), res.getMainGA()); + assertFalse(res.getReadGAs().contains(res.getMainGA())); + assertEquals(3, res.getListenGAs().size()); + assertEquals(0, res.getReadGAs().size()); + } + + @Test + void testParseWithoutDptSingleWithoutRead() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("1/3/22"); + + if (res == null) { + fail(); + return; + } + + assertNull(res.getDPT()); + assertEquals(new GroupAddress("1/3/22"), res.getMainGA()); + assertFalse(res.getReadGAs().contains(res.getMainGA())); + assertEquals(1, res.getListenGAs().size()); + assertEquals(0, res.getReadGAs().size()); + } + + @Test + void testParseWithoutDptSingleWithRead() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("<1/3/22"); + + if (res == null) { + fail(); + return; + } + + assertNull(res.getDPT()); + assertEquals(new GroupAddress("1/3/22"), res.getMainGA()); + assertTrue(res.getReadGAs().contains(res.getMainGA())); + assertEquals(1, res.getListenGAs().size()); + assertEquals(1, res.getReadGAs().size()); + } + + @Test + void testParseTwoLevel() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<3/1024+<4/1025"); + + if (res == null) { + fail(); + return; + } + + assertEquals(new GroupAddress("3/1024"), res.getMainGA()); + assertTrue(res.getReadGAs().contains(res.getMainGA())); + assertEquals(2, res.getListenGAs().size()); + assertEquals(2, res.getReadGAs().size()); + } + + @Test + void testParseFreeLevel() throws KNXFormatException { + GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<4610+<4611"); + + if (res == null) { + fail(); + return; + } + + assertEquals(new GroupAddress("4610"), res.getMainGA()); + assertEquals(2, res.getListenGAs().size()); + assertEquals(2, res.getReadGAs().size()); + } + + @Test + public void testChannelGaParsing() throws KNXFormatException { + Channel channel = mock(Channel.class); + Configuration configuration = new Configuration( + Map.of("key1", "5.001:<1/2/3+4/5/6+1/5/6", "key2", "1.001:7/1/9+1/1/2")); + when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("a:b:c")); + when(channel.getConfiguration()).thenReturn(configuration); + when(channel.getAcceptedItemType()).thenReturn("none"); + + MyKNXChannel knxChannel = new MyKNXChannel(channel); + + Set listenAddresses = knxChannel.getAllGroupAddresses(); + assertEquals(5, listenAddresses.size()); + // we don't check the content since parsing has been checked before and the quantity is correct + Set writeAddresses = knxChannel.getWriteAddresses(); + assertEquals(2, writeAddresses.size()); + assertTrue(writeAddresses.contains(new GroupAddress("1/2/3"))); + assertTrue(writeAddresses.contains(new GroupAddress("7/1/9"))); + } + + private static class MyKNXChannel extends KNXChannel { + public MyKNXChannel(Channel channel) { + super(Set.of("key1", "key2"), List.of(UnDefType.class), channel); + } + + @Override + protected String getDefaultDPT(String gaConfigKey) { + return ""; + } + } +} diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTypeTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTypeTest.java deleted file mode 100644 index 14bbbb1b791..00000000000 --- a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTypeTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.channel; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Collections; -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * - * @author Simon Kaufmann - initial contribution and API. - * - */ -@NonNullByDefault -class KNXChannelTypeTest { - - private KNXChannelType ct = new MyKNXChannelType(""); - - @BeforeEach - void setup() { - ct = new MyKNXChannelType(""); - } - - @Test - void testParseWithDptMultipleWithRead() { - ChannelConfiguration res = ct.parse("5.001:<1/3/22+0/3/22+<0/8/15"); - - if (res == null) { - fail(); - return; - } - - assertEquals("5.001", res.getDPT()); - assertEquals("1/3/22", res.getMainGA().getGA()); - assertTrue(res.getMainGA().isRead()); - assertEquals(3, res.getListenGAs().size()); - assertEquals(2, res.getReadGAs().size()); - } - - @Test - void testParseWithDptMultipleWithoutRead() { - ChannelConfiguration res = ct.parse("5.001:1/3/22+0/3/22+0/8/15"); - - if (res == null) { - fail(); - return; - } - - assertEquals("5.001", res.getDPT()); - assertEquals("1/3/22", res.getMainGA().getGA()); - assertFalse(res.getMainGA().isRead()); - assertEquals(3, res.getListenGAs().size()); - assertEquals(0, res.getReadGAs().size()); - } - - @Test - void testParseWithoutDptSingleWithoutRead() { - ChannelConfiguration res = ct.parse("1/3/22"); - - if (res == null) { - fail(); - return; - } - - assertNull(res.getDPT()); - assertEquals("1/3/22", res.getMainGA().getGA()); - assertFalse(res.getMainGA().isRead()); - assertEquals(1, res.getListenGAs().size()); - assertEquals(0, res.getReadGAs().size()); - } - - @Test - void testParseWithoutDptSingleWitRead() { - ChannelConfiguration res = ct.parse("<1/3/22"); - - if (res == null) { - fail(); - return; - } - - assertNull(res.getDPT()); - assertEquals("1/3/22", res.getMainGA().getGA()); - assertTrue(res.getMainGA().isRead()); - assertEquals(1, res.getListenGAs().size()); - assertEquals(1, res.getReadGAs().size()); - } - - @Test - void testParseTwoLevel() { - ChannelConfiguration res = ct.parse("5.001:<3/1024+<4/1025"); - - if (res == null) { - fail(); - return; - } - - assertEquals("3/1024", res.getMainGA().getGA()); - assertEquals(2, res.getListenGAs().size()); - assertEquals(2, res.getReadGAs().size()); - } - - @Test - void testParseFreeLevel() { - ChannelConfiguration res = ct.parse("5.001:<4610+<4611"); - - if (res == null) { - fail(); - return; - } - - assertEquals("4610", res.getMainGA().getGA()); - assertEquals(2, res.getListenGAs().size()); - assertEquals(2, res.getReadGAs().size()); - } - - private static class MyKNXChannelType extends KNXChannelType { - public MyKNXChannelType(String channelTypeID) { - super(channelTypeID); - } - - @Override - protected Set getAllGAKeys() { - return Collections.emptySet(); - } - - @Override - protected String getDefaultDPT(String gaConfigKey) { - return ""; - } - } -} diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/DPTTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/DPTTest.java new file mode 100644 index 00000000000..668276fca1d --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/DPTTest.java @@ -0,0 +1,411 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.dpt; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; + +import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator4ByteUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned; +import tuwien.auto.calimero.dptxlator.DptXlator2ByteSigned; + +/** + * + * @author Simon Kaufmann - Initial contribution + * + */ +@NonNullByDefault +class DPTTest { + + @Test + void testToDPTValueTrailingZeroesStrippedOff() { + assertEquals("3", ValueEncoder.encode(new DecimalType("3"), "17.001")); + assertEquals("3", ValueEncoder.encode(new DecimalType("3.0"), "17.001")); + } + + @Test + public void testToDPTValueDecimalType() { + assertEquals("23.1", ValueEncoder.encode(new DecimalType("23.1"), "9.001")); + } + + @Test + @SuppressWarnings("null") + void testToDPT5ValueFromQuantityType() { + assertEquals("80", ValueEncoder.encode(new QuantityType<>("80 %"), "5.001")); + + assertEquals("180", ValueEncoder.encode(new QuantityType<>("180 °"), "5.003")); + assertTrue(ValueEncoder.encode(new QuantityType<>("3.14 rad"), "5.003").startsWith("179.")); + assertEquals("80", ValueEncoder.encode(new QuantityType<>("80 %"), "5.004")); + } + + @Test + @SuppressWarnings("null") + void testToDPT7ValueFromQuantityType() { + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.002")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.003")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.004")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.005")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 s"), "7.006")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 min"), "7.007")); + + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 m"), "7.011")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 mA"), "7.012")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 lx"), "7.013")); + + assertEquals("3000", ValueEncoder.encode(new QuantityType<>("3000 K"), "7.600")); + } + + @Test + @SuppressWarnings("null") + void testToDPT8ValueFromQuantityType() { + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.002")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.003")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.004")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.005")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 s"), "8.006")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 min"), "8.007")); + + assertEquals("180", ValueEncoder.encode(new QuantityType<>("180 °"), "8.011")); + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 km"), "8.012")); + } + + @Test + @SuppressWarnings("null") + void testToDPT9ValueFromQuantityType() { + assertEquals("23.1", ValueEncoder.encode(new QuantityType<>("23.1 °C"), "9.001")); + assertEquals(5.0, + Double.parseDouble(Objects.requireNonNull(ValueEncoder.encode(new QuantityType<>("41 °F"), "9.001")))); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("274.15 K"), "9.001")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "9.002")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 mK"), "9.002")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C"), "9.002")); + assertTrue(ValueEncoder.encode(new QuantityType<>("1 °F"), "9.002").startsWith("0.55")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K/h"), "9.003")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C/h"), "9.003")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 mK/h"), "9.003")); + assertEquals("600", ValueEncoder.encode(new QuantityType<>("10 K/min"), "9.003")); + assertEquals("100", ValueEncoder.encode(new QuantityType<>("100 lx"), "9.004")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s"), "9.005")); + assertTrue(ValueEncoder.encode(new QuantityType<>("1.94 kn"), "9.005").startsWith("0.99")); + assertEquals(1.0, Double + .parseDouble(Objects.requireNonNull(ValueEncoder.encode(new QuantityType<>("3.6 km/h"), "9.005")))); + assertEquals("456", ValueEncoder.encode(new QuantityType<>("456 Pa"), "9.006")); + assertEquals("70", ValueEncoder.encode(new QuantityType<>("70 %"), "9.007")); + assertEquals("8", ValueEncoder.encode(new QuantityType<>("8 ppm"), "9.008")); + assertEquals("9", ValueEncoder.encode(new QuantityType<>("9 m³/h"), "9.009")); + assertEquals("10", ValueEncoder.encode(new QuantityType<>("10 s"), "9.010")); + assertEquals("11", ValueEncoder.encode(new QuantityType<>("0.011 s"), "9.011")); + + assertEquals("20", ValueEncoder.encode(new QuantityType<>("20 mV"), "9.020")); + assertEquals("20", ValueEncoder.encode(new QuantityType<>("0.02 V"), "9.020")); + assertEquals("21", ValueEncoder.encode(new QuantityType<>("21 mA"), "9.021")); + assertEquals("21", ValueEncoder.encode(new QuantityType<>("0.021 A"), "9.021")); + assertEquals("12", ValueEncoder.encode(new QuantityType<>("12 W/m²"), "9.022")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K/%"), "9.023")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C/%"), "9.023")); + assertTrue(ValueEncoder.encode(new QuantityType<>("1 °F/%"), "9.023").startsWith("0.55")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kW"), "9.024")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/h"), "9.025")); + assertEquals("60", ValueEncoder.encode(new QuantityType<>("1 l/min"), "9.025")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/m²"), "9.026")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °F"), "9.027")); + assertTrue(ValueEncoder.encode(new QuantityType<>("-12 °C"), "9.027").startsWith("10.")); + assertEquals("10", ValueEncoder.encode(new QuantityType<>("10 km/h"), "9.028")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 g/m³"), "9.029")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 µg/m³"), "9.030")); + } + + @Test + @SuppressWarnings("null") + void testToDPT10ValueFromQuantityType() { + // DateTimeTyype, not QuantityType + assertEquals("Wed, 17:30:00", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "10.001")); + } + + @Test + @SuppressWarnings("null") + void testToDPT11ValueFromQuantityType() { + // DateTimeTyype, not QuantityType + assertEquals("2019-06-12", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "11.001")); + } + + @Test + @SuppressWarnings("null") + void testToDPT12ValueFromQuantityType() { + // 12.001: dimensionless + + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 s"), "12.100")); + assertEquals("2", ValueEncoder.encode(new QuantityType<>("2 min"), "12.101")); + assertEquals("3", ValueEncoder.encode(new QuantityType<>("3 h"), "12.102")); + + assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 m^3"), "12.1200")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l"), "12.1200")); + assertEquals("2", ValueEncoder.encode(new QuantityType<>("2 m³"), "12.1201")); + } + + @Test + @SuppressWarnings("null") + void testToDPT13ValueFromQuantityType() { + // 13.001 dimensionless + assertEquals("24", ValueEncoder.encode(new QuantityType<>("24 m³/h"), "13.002")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("24 m³/d"), "13.002")); + + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 Wh"), "13.010")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 VAh"), "13.011")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 varh"), "13.012")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 kWh"), "13.013")); + assertEquals("4.2", ValueEncoder.encode(new QuantityType<>("4200 VAh"), "13.014")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 kvarh"), "13.015")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 MWh"), "13.016")); + + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 s"), "13.100")); + + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 l"), "13.1200")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 m³"), "13.1201")); + } + + @Test + @SuppressWarnings("null") + void testToDPT14ValueFromQuantityType() { + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s²"), "14.000")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s²"), "14.001")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/mol"), "14.002")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 /s"), "14.003")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 mol"), "14.004")); + + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad"), "14.006")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °"), "14.007")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J*s"), "14.008")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s"), "14.009")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m²"), "14.010")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 F"), "14.011")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.012")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m³"), "14.013")); + + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m²/N"), "14.014")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 S"), "14.015")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 S/m"), "14.016")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg/m³"), "14.017")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C"), "14.018")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A"), "14.019")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m²"), "14.020")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C*m"), "14.021")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.022")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V/m"), "14.023")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C"), "14.024")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.025")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.026")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.027")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.028")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A*m²"), "14.029")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.030")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.031")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N"), "14.032")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Hz"), "14.033")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s"), "14.034")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/K"), "14.035")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W"), "14.036")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.037")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.038")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m"), "14.039")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.040")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 lm*s"), "14.040")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 cd/m²"), "14.041")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 lm"), "14.042")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 cd"), "14.043")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m"), "14.044")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Wb"), "14.045")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 T"), "14.046")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A*m²"), "14.047")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 T"), "14.048")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m"), "14.049")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A"), "14.050")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg"), "14.051")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg/s"), "14.052")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N/s"), "14.053")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad"), "14.054")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °"), "14.055")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W"), "14.056")); + // 14.057: dimensionless + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Pa"), "14.058")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.059")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.060")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm*m"), "14.061")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 H"), "14.062")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 sr"), "14.063")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W/m²"), "14.064")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s"), "14.065")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Pa"), "14.066")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N/m"), "14.067")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C"), "14.068")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "14.069")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "14.070")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/K"), "14.071")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W/m/K"), "14.072")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V/K"), "14.073")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 s"), "14.074")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N*m"), "14.075")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.075")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³"), "14.076")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³/s"), "14.077")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N"), "14.078")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.079")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 VA"), "14.080")); + + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³/h"), "14.1200")); + assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/s"), "14.1201")); + } + + @Test + @SuppressWarnings("null") + void testToDPT19ValueFromQuantityType() { + // DateTimeTyype, not QuantityType + assertEquals("2019-06-12 17:30:00", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "19.001")); + } + + @Test + @SuppressWarnings("null") + void testToDPT29ValueFromQuantityType() { + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 Wh"), "29.010")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 VAh"), "29.011")); + assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 varh"), "29.012")); + } + + @Test + public void dpt232RgbValue() { + // input data + byte[] data = new byte[] { 123, 45, 67 }; + + // this is the old implementation + String value = "r:123 g:45 b:67"; + int r = Integer.parseInt(value.split(" ")[0].split(":")[1]); + int g = Integer.parseInt(value.split(" ")[1].split(":")[1]); + int b = Integer.parseInt(value.split(" ")[2].split(":")[1]); + HSBType expected = HSBType.fromRGB(r, g, b); + + assertEquals(expected, ValueDecoder.decode("232.600", data, HSBType.class)); + } + + @Test + public void dpt232HsbValue() { + // input data + byte[] data = new byte[] { 123, 45, 67 }; + + HSBType hsbType = (HSBType) ValueDecoder.decode("232.60000", data, HSBType.class); + + Assertions.assertNotNull(hsbType); + Objects.requireNonNull(hsbType); + assertEquals(173.6, hsbType.getHue().doubleValue(), 0.1); + assertEquals(17.6, hsbType.getSaturation().doubleValue(), 0.1); + assertEquals(26.3, hsbType.getBrightness().doubleValue(), 0.1); + } + + @Test + public void dpt252EncoderTest() { + // input data + byte[] data = new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e }; + HSBType hsbType = (HSBType) ValueDecoder.decode("251.600", data, HSBType.class); + + assertNotNull(hsbType); + assertEquals(207, hsbType.getHue().doubleValue(), 0.1); + assertEquals(22, hsbType.getSaturation().doubleValue(), 0.1); + assertEquals(18, hsbType.getBrightness().doubleValue(), 0.1); + } + + // This test checks all our overrides for units. It allows to detect unnecessary overrides when we + // update Calimero library + @Test + public void unitFixes() { + // 8bit signed (DPT 6) + assertEquals(DPTXlator8BitSigned.DPT_PERCENT_V8.getUnit(), Units.PERCENT.getSymbol()); + + // two byte unsigned (DPT 7) + assertNotEquals("", DPTXlator2ByteUnsigned.DPT_VALUE_2_UCOUNT.getUnit()); // counts have no unit + assertNotEquals(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_10.getUnit(), "ms"); // according to spec, it is ms + assertNotEquals(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_100.getUnit(), "ms"); // according to spec, it is ms + + // two byte signed (DPT 8, DPTXlator is missing in calimero 2.5-M1) + assertNotEquals("", DptXlator2ByteSigned.DptValueCount.getUnit()); // pulses habe no unit + + // 4 byte unsigned (DPT 12) + assertNotEquals("", DPTXlator4ByteUnsigned.DPT_VALUE_4_UCOUNT.getUnit()); // counts have no unit + + // 4 byte signed (DPT 13) + assertNotEquals(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY.getUnit(), Units.VAR_HOUR.toString()); + assertNotEquals(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY_KVARH.getUnit(), Units.KILOVAR_HOUR.toString()); + assertNotEquals(DPTXlator4ByteSigned.DPT_APPARENT_ENERGY_KVAH.getUnit(), "kVA*h"); + assertNotEquals(DPTXlator4ByteSigned.DPT_FLOWRATE.getUnit(), Units.CUBICMETRE_PER_HOUR.toString()); + assertNotEquals("", DPTXlator4ByteSigned.DPT_COUNT.getUnit()); // counts have no unit + + // four byte float (DPT 14) + assertNotEquals(DPTXlator4ByteFloat.DPT_CONDUCTANCE.getUnit(), Units.SIEMENS.toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ANGULAR_MOMENTUM.getUnit(), + Units.JOULE.multiply(Units.SECOND).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ACTIVITY.getUnit(), Units.BECQUEREL.toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRICAL_CONDUCTIVITY.getUnit(), + Units.SIEMENS.divide(SIUnits.METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_TORQUE.getUnit(), Units.NEWTON.multiply(SIUnits.METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_RESISTIVITY.getUnit(), Units.OHM.multiply(SIUnits.METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRIC_DIPOLEMOMENT.getUnit(), + Units.COULOMB.multiply(SIUnits.METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getUnit(), Units.VOLT.multiply(SIUnits.METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_MAGNETIC_MOMENT.getUnit(), + Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString()); + assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTROMAGNETIC_MOMENT.getUnit(), + Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString()); + + // 64 bit signed (DPT 29) + assertNotEquals(DPTXlator64BitSigned.DPT_REACTIVE_ENERGY.getUnit(), Units.VAR_HOUR.toString()); + } + + private static Stream unitProvider() { + return DPTUnits.getAllUnitStrings(); + } + + @ParameterizedTest + @MethodSource("unitProvider") + public void unitsValid(String unit) { + String valueStr = "1 " + unit; + QuantityType value = new QuantityType<>(valueStr); + Assertions.assertNotNull(value); + } + + private static Stream rgbValueProvider() { + return Stream.of("r:0 g:0 b:0", "r:255 g:255 b:255"); + } + + @ParameterizedTest + @MethodSource("rgbValueProvider") + public void rgbTest(String value) { + Assertions.assertNotNull(ValueDecoder.decode("232.600", value.getBytes(StandardCharsets.UTF_8), HSBType.class)); + } +} diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapperTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapperTest.java deleted file mode 100644 index deb5aa59f24..00000000000 --- a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapperTest.java +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.knx.internal.dpt; - -import static org.junit.jupiter.api.Assertions.*; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.QuantityType; - -/** - * - * @author Simon Kaufmann - initial contribution and API - * - */ -@NonNullByDefault -class KNXCoreTypeMapperTest { - - @Test - void testToDPTValueTrailingZeroesStrippedOff() { - assertEquals("3", new KNXCoreTypeMapper().toDPTValue(new DecimalType("3"), "17.001")); - assertEquals("3", new KNXCoreTypeMapper().toDPTValue(new DecimalType("3.0"), "17.001")); - } - - @Test - @SuppressWarnings("null") - void testToDPT5ValueFromQuantityType() { - assertEquals("80.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("80 %"), "5.001")); - - assertEquals("180.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("180 °"), "5.003")); - assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3.14 rad"), "5.003").startsWith("179.")); - assertEquals("80.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("80 %"), "5.004")); - } - - @Test - @SuppressWarnings("null") - void testToDPT7ValueFromQuantityType() { - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.002")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.003")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.004")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.005")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 s"), "7.006")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 min"), "7.007")); - - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m"), "7.011")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mA"), "7.012")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 lx"), "7.013")); - - assertEquals("3000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3000 K"), "7.600")); - } - - @Test - @SuppressWarnings("null") - void testToDPT8ValueFromQuantityType() { - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.002")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.003")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.004")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.005")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 s"), "8.006")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 min"), "8.007")); - - assertEquals("180.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("180 °"), "8.011")); - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 km"), "8.012")); - } - - @Test - @SuppressWarnings("null") - void testToDPT9ValueFromQuantityType() { - assertEquals("23.1", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("23.1 °C"), "9.001")); - assertEquals("5.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("41 °F"), "9.001")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("274.15 K"), "9.001")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "9.002")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mK"), "9.002")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C"), "9.002")); - assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F"), "9.002").startsWith("0.55")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K/h"), "9.003")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C/h"), "9.003")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mK/h"), "9.003")); - assertEquals("600.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 K/min"), "9.003")); - assertEquals("100.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("100 lx"), "9.004")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s"), "9.005")); - assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1.94 kn"), "9.005").startsWith("0.99")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3.6 km/h"), "9.005")); - assertEquals("456.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("456 Pa"), "9.006")); - assertEquals("70.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("70 %"), "9.007")); - assertEquals("8.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("8 ppm"), "9.008")); - assertEquals("9.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("9 m³/h"), "9.009")); - assertEquals("10.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 s"), "9.010")); - assertEquals("11.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.011 s"), "9.011")); - - assertEquals("20.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("20 mV"), "9.020")); - assertEquals("20.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.02 V"), "9.020")); - assertEquals("21.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("21 mA"), "9.021")); - assertEquals("21.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.021 A"), "9.021")); - assertEquals("12.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("12 W/m²"), "9.022")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K/%"), "9.023")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C/%"), "9.023")); - assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F/%"), "9.023").startsWith("0.55")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kW"), "9.024")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/h"), "9.025")); - assertEquals("60.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/min"), "9.025")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/m²"), "9.026")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F"), "9.027")); - assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("-12 °C"), "9.027").startsWith("10.")); - assertEquals("10.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 km/h"), "9.028")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 g/m³"), "9.029")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 µg/m³"), "9.030")); - } - - @Test - @SuppressWarnings("null") - void testToDPT10ValueFromQuantityType() { - // DateTimeTyype, not QuantityType - assertEquals("Wed, 17:30:00", - new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "10.001")); - } - - @Test - @SuppressWarnings("null") - void testToDPT11ValueFromQuantityType() { - // DateTimeTyype, not QuantityType - assertEquals("2019-06-12", - new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "11.001")); - } - - @Test - @SuppressWarnings("null") - void testToDPT12ValueFromQuantityType() { - // 12.001: dimensionless - - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 s"), "12.100")); - assertEquals("2.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("2 min"), "12.101")); - assertEquals("3.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3 h"), "12.102")); - - assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m^3"), "12.1200")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l"), "12.1200")); - assertEquals("2.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("2 m³"), "12.1201")); - } - - @Test - @SuppressWarnings("null") - void testToDPT13ValueFromQuantityType() { - // 13.001 dimensionless - assertEquals("24.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("24 m³/h"), "13.002")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("24 m³/d"), "13.002")); - - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 Wh"), "13.010")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 VAh"), "13.011")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 varh"), "13.012")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 kWh"), "13.013")); - assertEquals("4.2", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("4200 VAh"), "13.014")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 kvarh"), "13.015")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 MWh"), "13.016")); - - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 s"), "13.100")); - - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 l"), "13.1200")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 m³"), "13.1201")); - } - - @Test - @SuppressWarnings("null") - void testToDPT14ValueFromQuantityType() { - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s²"), "14.000")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s²"), "14.001")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/mol"), "14.002")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 /s"), "14.003")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 mol"), "14.004")); - - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad"), "14.006")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °"), "14.007")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J*s"), "14.008")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s"), "14.009")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m²"), "14.010")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 F"), "14.011")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.012")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m³"), "14.013")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m²/N"), "14.014")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 S"), "14.015")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 S/m"), "14.016")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg/m³"), "14.017")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C"), "14.018")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A"), "14.019")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m²"), "14.020")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C*m"), "14.021")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.022")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V/m"), "14.023")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C"), "14.024")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.025")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.026")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.027")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.028")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A*m²"), "14.029")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.030")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.031")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N"), "14.032")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Hz"), "14.033")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s"), "14.034")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/K"), "14.035")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W"), "14.036")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.037")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.038")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m"), "14.039")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.040")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 lm*s"), "14.040")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 cd/m²"), "14.041")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 lm"), "14.042")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 cd"), "14.043")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m"), "14.044")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Wb"), "14.045")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 T"), "14.046")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A*m²"), "14.047")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 T"), "14.048")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m"), "14.049")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A"), "14.050")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg"), "14.051")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg/s"), "14.052")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N/s"), "14.053")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad"), "14.054")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °"), "14.055")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W"), "14.056")); - // 14.057: dimensionless - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Pa"), "14.058")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.059")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.060")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm*m"), "14.061")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 H"), "14.062")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 sr"), "14.063")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W/m²"), "14.064")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s"), "14.065")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Pa"), "14.066")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N/m"), "14.067")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C"), "14.068")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "14.069")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "14.070")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/K"), "14.071")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W/m/K"), "14.072")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V/K"), "14.073")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 s"), "14.074")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N*m"), "14.075")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.075")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³"), "14.076")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³/s"), "14.077")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N"), "14.078")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.079")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 VA"), "14.080")); - - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³/h"), "14.1200")); - assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/s"), "14.1201")); - } - - @Test - @SuppressWarnings("null") - void testToDPT19ValueFromQuantityType() { - // DateTimeTyype, not QuantityType - assertEquals("2019-06-12 17:30:00", - new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "19.001")); - } - - @Test - @SuppressWarnings("null") - void testToDPT29ValueFromQuantityType() { - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 Wh"), "29.010")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 VAh"), "29.011")); - assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 varh"), "29.012")); - } -}