]> git.basschouten.com Git - openhab-addons.git/commitdiff
[knx] Refactoring of KnxCoreTypeMapper and UOM Support (#14534)
authorHolger Friedrich <holgerfriedrich@users.noreply.github.com>
Fri, 17 Mar 2023 11:50:13 +0000 (12:50 +0100)
committerGitHub <noreply@github.com>
Fri, 17 Mar 2023 11:50:13 +0000 (12:50 +0100)
* [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 <github@klug.nrw>
Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
45 files changed:
CODEOWNERS
bundles/org.openhab.binding.knx/README.md
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ChannelConfiguration.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/GroupAddressConfiguration.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannel.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelType.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/KNXChannelTypes.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadRequestSpecImpl.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ReadResponseSpecImpl.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeContact.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDateTime.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeNumber.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeString.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeSwitch.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/WriteSpecImpl.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/BusMessageListener.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/InboundSpec.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/KNXClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/NoOpClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/OutboundSpec.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/StatusUpdateCallback.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/DeviceConfig.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUnits.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/DPTUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapper.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueDecoder.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/dpt/ValueEncoder.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java [deleted file]
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java
bundles/org.openhab.binding.knx/src/main/resources/OH-INF/addon/addon.xml
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelFactoryTest.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTest.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/channel/KNXChannelTypeTest.java [deleted file]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/DPTTest.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/dpt/KNXCoreTypeMapperTest.java [deleted file]

index 69fc52c11ce4ed6345a0b99f7c4f41f9a077b4dc..210554db57d55d80826e2c6249c0a9c2bf768f2f 100644 (file)
 /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
index 21b62a106ad0d5d06c445282d6998fcdc16140cd..3b5348d5fb68d07e873e534fb220a309135a7e26 100644 (file)
@@ -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.
-
-##### Channel Type "switch"
+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.
 
-| 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       |
-
-##### Channel Type "color"
-
-| 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     |
+| position         | Group address brightness               | 5.001       |
+| increaseDecrease | Group address for relative brightness  | 3.007       |
 
-##### Channel Type "rollershutter"
+The `hsb` address supports DPT 242.600 and 251.600.
 
-| 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       |
+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 "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"
+##### 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: Using the Units Of Measurement feature of openHAB (Quantitytype) requires that the DPT value is set correctly.
+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.
 
-##### Channel Type "string"
+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 "datetime"
+##### Channel Type `switch`, `switch-control`
 
-| Parameter | Description   | Default DPT |
-|-----------|---------------|-------------|
-| ga        | Group address | 19.001      |
+| 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]"               <light>          { channel="knx:device:bridge:generic:demoSwitch" }
-Color         demoColorLight     "Color [%s]"               <light>          { channel="knx:device:bridge:generic:demoColorLight" }
-Dimmer        demoDimmer         "Dimmer [%d %%]"           <light>          { channel="knx:device:bridge:generic:demoDimmer" }
-Rollershutter demoRollershutter  "Shade [%d %%]"            <rollershutter>  { channel="knx:device:bridge:generic:demoRollershutter" }
-Contact       demoContact        "Front Door [%s]"          <frontdoor>      { channel="knx:device:bridge:generic:demoContact" }
-Number        demoTemperature    "Temperature [%.1f °C]"    <temperature>    { 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]"               <light>          { channel="knx:device:bridge:generic:demoSwitch" }
+Color               demoColorLight     "Color [%s]"               <light>          { channel="knx:device:bridge:generic:demoColorLight" }
+Dimmer              demoDimmer         "Dimmer [%d %%]"           <light>          { channel="knx:device:bridge:generic:demoDimmer" }
+Rollershutter       demoRollershutter  "Shade [%d %%]"            <rollershutter>  { channel="knx:device:bridge:generic:demoRollershutter" }
+Contact             demoContact        "Front Door [%s]"          <frontdoor>      { channel="knx:device:bridge:generic:demoContact" }
+Number:Temperature  demoTemperature    "Temperature [%.1f °C]"    <temperature>    { 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:
index b2557e545f731aaa7797151e44534fdaad9d6183..7e75da0f1b8fd6431ea2b059094a14c9b5e412a8 100644 (file)
  */
 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<String> CONTROL_CHANNEL_TYPES = Collections.unmodifiableSet(Stream.of(CHANNEL_COLOR_CONTROL, //
+    public static final Set<String> 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 (file)
index 4a0892c..0000000
+++ /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.
-     * <p>
-     * 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 (file)
index 9d6545a..0000000
+++ /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<GroupAddressConfiguration> listenGAs;
-
-    public ChannelConfiguration(@Nullable String dpt, GroupAddressConfiguration mainGA,
-            List<GroupAddressConfiguration> listenGAs) {
-        this.dpt = dpt;
-        this.mainGA = mainGA;
-        this.listenGAs = listenGAs;
-    }
-
-    public @Nullable String getDPT() {
-        return dpt;
-    }
-
-    public GroupAddressConfiguration getMainGA() {
-        return mainGA;
-    }
-
-    public List<GroupAddressConfiguration> getListenGAs() {
-        return Stream.concat(Stream.of(mainGA), listenGAs.stream()).collect(toList());
-    }
-
-    public List<GroupAddressConfiguration> getReadGAs() {
-        return getListenGAs().stream().filter(ga -> ga.isRead()).collect(toList());
-    }
-}
index b4d3643c4bbed0d923a976251edffba5e53a52a1..a24ff68c54029baf1f963999faeed33c09b996e8 100644 (file)
  */
 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 static final Pattern PATTERN_GA_CONFIGURATION = Pattern.compile(
+            "^((?<dpt>[1-9][0-9]{0,2}\\.[0-9]{3,5}):)?(?<read><)?(?<mainGA>[0-9]{1,5}(/[0-9]{1,4}){0,2})(?<listenGAs>(\\+(<?[0-9]{1,5}(/[0-9]{1,4}){0,2}))*)$");
+    private static final Pattern PATTERN_LISTEN_GA = Pattern
+            .compile("\\+((?<read><)?(?<GA>[0-9]{1,5}(/[0-9]{1,4}){0,2}))");
+
+    private final @Nullable String dpt;
+    private final GroupAddress mainGA;
+    private final Set<GroupAddress> listenGAs;
+    private final Set<GroupAddress> readGAs;
+
+    private GroupAddressConfiguration(@Nullable String dpt, GroupAddress mainGA, Set<GroupAddress> listenGAs,
+            Set<GroupAddress> readGAs) {
+        this.dpt = dpt;
+        this.mainGA = mainGA;
+        this.listenGAs = listenGAs;
+        this.readGAs = readGAs;
+    }
 
-    private final String ga;
-    private final boolean read;
+    public @Nullable String getDPT() {
+        return dpt;
+    }
+
+    public GroupAddress getMainGA() {
+        return mainGA;
+    }
 
-    public GroupAddressConfiguration(String ga, boolean read) {
-        super();
-        this.ga = ga;
-        this.read = read;
+    public Set<GroupAddress> getListenGAs() {
+        return listenGAs;
     }
 
-    /**
-     * The group address.
-     *
-     * @return the group address.
-     */
-    public String getGA() {
-        return ga;
+    public Set<GroupAddress> getReadGAs() {
+        return readGAs;
     }
 
-    /**
-     * 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 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<GroupAddress> listenGAs = new HashSet<>();
+            Set<GroupAddress> 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 (file)
index 0000000..01e4079
--- /dev/null
@@ -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<String> gaKeys;
+
+    private final Map<String, GroupAddressConfiguration> groupAddressConfigurations = new HashMap<>();
+    private final Set<GroupAddress> listenAddresses = new HashSet<>();
+    private final Set<GroupAddress> writeAddresses = new HashSet<>();
+    private final String channelType;
+    private final ChannelUID channelUID;
+    private final boolean isControl;
+    private final Class<? extends Type> preferredType;
+
+    KNXChannel(List<Class<? extends Type>> acceptedTypes, Channel channel) {
+        this(Set.of(GA), acceptedTypes, channel);
+    }
+
+    KNXChannel(Set<String> gaKeys, List<Class<? extends Type>> 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<Class<? extends Type>> 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<? extends Type> preferredType() {
+        return preferredType;
+    }
+
+    public final Set<GroupAddress> getAllGroupAddresses() {
+        return listenAddresses;
+    }
+
+    public final Set<GroupAddress> getWriteAddresses() {
+        return writeAddresses;
+    }
+
+    public final @Nullable OutboundSpec getCommandSpec(Type command) {
+        logger.trace("getCommandSpec checking keys '{}' for command '{}' ({})", gaKeys, command, command.getClass());
+        for (Map.Entry<String, GroupAddressConfiguration> entry : groupAddressConfigurations.entrySet()) {
+            String dpt = Objects.requireNonNullElse(entry.getValue().getDPT(), getDefaultDPT(entry.getKey()));
+            Set<Class<? extends Type>> 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<InboundSpec> 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 (file)
index 0000000..826ed57
--- /dev/null
@@ -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<Set<String>, Function<Channel, KNXChannel>> 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<Channel, KNXChannel> 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 (file)
index 170499f..0000000
+++ /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(
-            "^((?<dpt>[0-9]{1,3}\\.[0-9]{3,4}):)?(?<read>\\<)?(?<mainGA>[0-9]{1,5}(/[0-9]{1,4}){0,2})(?<listenGAs>(\\+(\\<?[0-9]{1,5}(/[0-9]{1,4}){0,2}))*)$");
-
-    private static final Pattern PATTERN_LISTEN = Pattern
-            .compile("\\+((?<read>\\<)?(?<GA>[0-9]{1,5}(/[0-9]{1,4}){0,2}))");
-
-    private final Logger logger = LoggerFactory.getLogger(KNXChannelType.class);
-    private final Set<String> channelTypeIDs;
-
-    KNXChannelType(String... channelTypeIDs) {
-        this.channelTypeIDs = new HashSet<>(Arrays.asList(channelTypeIDs));
-    }
-
-    final Set<String> 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<GroupAddressConfiguration> 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<String> getAllGAKeys();
-
-    public final Set<GroupAddress> getListenAddresses(Configuration channelConfiguration) {
-        Set<GroupAddress> 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<GroupAddress> getReadAddresses(Configuration channelConfiguration) {
-        Set<GroupAddress> 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<GroupAddress> getWriteAddresses(Configuration channelConfiguration) {
-        Set<GroupAddress> 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<GroupAddress> getAddresses(@Nullable Configuration configuration, Iterable<String> addresses)
-            throws KNXFormatException {
-        Set<GroupAddress> 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<String> 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<? extends Type> 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<InboundSpec> 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<ListenSpecImpl> 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<ReadResponseSpecImpl> 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 (file)
index 145b1a3..0000000
+++ /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<KNXChannelType> 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");
-    }
-}
index e56ca5d39735a369e9007d8caccb945c5a5598ea..cd286dbf4181cab6589f30d0113f7cf18fa581d9 100644 (file)
  */
 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<GroupAddress> listenAddresses;
 
-    private final List<GroupAddress> listenAddresses;
+    public ListenSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) {
+        this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
+        this.listenAddresses = groupAddressConfiguration.getListenGAs();
+    }
 
-    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();
-        }
+    @Override
+    public String getDPT() {
+        return dpt;
     }
 
-    public List<GroupAddress> getGroupAddresses() {
+    @Override
+    public Set<GroupAddress> getGroupAddresses() {
         return listenAddresses;
     }
 }
index e3f8d31abc85aeb2932670826003616652234779..6e3173b79066ae85b7dd080a682a5383e68a3cbe 100644 (file)
  */
 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<GroupAddress> readAddresses;
 
-    private final List<GroupAddress> readAddresses;
+    public ReadRequestSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) {
+        this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
+        this.readAddresses = groupAddressConfiguration.getReadGAs();
+    }
 
-    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();
-        }
+    @Override
+    public String getDPT() {
+        return dpt;
     }
 
     @Override
-    public List<GroupAddress> getGroupAddresses() {
+    public Set<GroupAddress> getGroupAddresses() {
         return readAddresses;
     }
 }
index 5a7ecbbd6bdd3b60f6f3ba18e6e6aa9dcb8187f2..6c17aa87518ba5163e281a44307a82eac1437fe1 100644 (file)
@@ -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 {
-
-    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 class ReadResponseSpecImpl implements OutboundSpec {
+    private final String dpt;
+    private final GroupAddress groupAddress;
+    private final Type value;
+
+    public ReadResponseSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type state) {
+        this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
+        this.groupAddress = groupAddressConfiguration.getMainGA();
+        this.value = state;
+    }
+
+    @Override
+    public String getDPT() {
+        return dpt;
     }
 
     @Override
-    public @Nullable GroupAddress getGroupAddress() {
+    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);
     }
 }
index e79fb4e36fd779702f08e78b7e2303ebea351c10..527af7911470b86abca5a3341118e0cdb0ab31e8 100644 (file)
  */
 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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_COLOR, CHANNEL_COLOR_CONTROL);
 
-    TypeColor() {
-        super(CHANNEL_COLOR, CHANNEL_COLOR_CONTROL);
-    }
-
-    @Override
-    protected Set<String> 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
index 363b47699049a481971f8a8b501a7cf43141285b..c53db439ad806916075b308f1b763719fc8b0504 100644 (file)
@@ -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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL);
 
-    TypeContact() {
-        super(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL);
-    }
-
-    @Override
-    protected Set<String> getAllGAKeys() {
-        return Collections.singleton(GA);
+    TypeContact(Channel channel) {
+        super(List.of(OpenClosedType.class), channel);
     }
 
     @Override
index b2e36274b4499a0e86eaac80998a362bae64a0f2..d9a3d9ccea1233a5927d2aa956ad1558b9a0f21c 100644 (file)
@@ -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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL);
 
-    TypeDateTime() {
-        super(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL);
-    }
-
-    @Override
-    protected Set<String> getAllGAKeys() {
-        return Collections.singleton(GA);
+    TypeDateTime(Channel channel) {
+        super(List.of(DateTimeType.class), channel);
     }
 
     @Override
index 5e01f0386ba8d528e967673f79bb73f1fbc6f36d..94006c4c4892e132a2c26d1f9b4e826a65f2e484 100644 (file)
@@ -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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL);
 
-    TypeDimmer() {
-        super(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL);
-    }
-
-    @Override
-    protected Set<String> 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
index f5ea76fe75413398c799426012eb6411ae954892..8caf885fd8e6dc5b61eda6cb1eb3d8e5a0658d8e 100644 (file)
@@ -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<String> 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<String> getAllGAKeys() {
-        return Collections.singleton(GA);
-    }
 }
index 94a41563d01d7b592e931e2910ba6644e987dbc1..dfe037bd4ba50cfc36e446265ff73000fdd22c03 100644 (file)
@@ -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<String> 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<String> getAllGAKeys() {
-        return Set.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA);
-    }
 }
index 4c755b126fdf3fab0b389fe66e80bb3992eca391..8a093712be7a5a22f833ef789f70a99942316aba 100644 (file)
@@ -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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_STRING, CHANNEL_STRING_CONTROL);
 
-    TypeString() {
-        super(CHANNEL_STRING, CHANNEL_STRING_CONTROL);
-    }
-
-    @Override
-    protected Set<String> getAllGAKeys() {
-        return Collections.singleton(GA);
+    TypeString(Channel channel) {
+        super(List.of(StringType.class), channel);
     }
 
     @Override
index cbf415328fbeac784dd103b9f4fccecb03f9eb4a..b3a2fb2d82134bdcaabf49e69682cad9994c3845 100644 (file)
@@ -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<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL);
 
-    TypeSwitch() {
-        super(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL);
-    }
-
-    @Override
-    protected Set<String> getAllGAKeys() {
-        return Collections.singleton(GA);
+    TypeSwitch(Channel channel) {
+        super(List.of(OnOffType.class), channel);
     }
 
     @Override
index ef5bb50917c2e37b40b2d5560b91e5dc0c1c7629..3e3b314a3a945912467aa295c71906db62f9650d 100644 (file)
  */
 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 {
-
-    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 class WriteSpecImpl implements OutboundSpec {
+    private final String dpt;
+    private final Type value;
+    private final GroupAddress groupAddress;
+
+    public WriteSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type value) {
+        this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
+        this.groupAddress = groupAddressConfiguration.getMainGA();
+        this.value = value;
+    }
+
+    @Override
+    public String getDPT() {
+        return dpt;
     }
 
     @Override
-    public Type getType() {
-        return type;
+    public Type getValue() {
+        return value;
     }
 
     @Override
-    public @Nullable GroupAddress getGroupAddress() {
+    public GroupAddress getGroupAddress() {
         return groupAddress;
     }
+
+    @Override
+    public boolean matchesDestination(GroupAddress groupAddress) {
+        return groupAddress.equals(this.groupAddress);
+    }
 }
index 2ef00e9aa713e33955e71e3b8d005c582c3ca9b6..df3d42e34a58c48b21fdaefec7bd621719db413e 100644 (file)
@@ -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);
index 37093dba91611e35767a2d21b4d6ae07a232576e..73026f85c3850f0bfb142251406e4b2aaa06f8e7 100644 (file)
@@ -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);
 }
index ffa3deaaa9a3b62e933da3c877b0ed24f7712779..e5145b0180432a7724a6fe7ebc09e563167f2baf 100644 (file)
@@ -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<GroupAddress> getGroupAddresses();
+    Set<GroupAddress> getGroupAddresses();
 }
index cbbc98b0138951dc9871bc6ee82389090a3fc7d5..c54991c902a31a84c9e2afffa5621ca8923c34ab 100644 (file)
@@ -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.
index 9bcc7382ce86cf36e3dc6f571ecf599492f86fee..92d1f3131e942d3ba70accfe430ba97f1a33c1de 100644 (file)
@@ -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
index f2951a1eec97300f11ab7593c44f6377590e0ad9..e8e349c31837cc34a7cbe1440603f614fcc9de9c 100644 (file)
@@ -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);
 }
index b4544216750d8d2fd4081502c8663a8647c30035..80b506b1fb0aec4cdb7844ee4448fd8e51b980fb 100644 (file)
@@ -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);
 }
index ece9352815b56423464d54ee260ae40702845ce5..eb144adea111070e3ed2e3b533742ad97b8dbcb1 100644 (file)
@@ -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
index 5746b6a86d5b13a45de90beefffaabdf03e4f2c6..207c2bce0c51e6fb8923efcbdfaefe196430f384 100644 (file)
@@ -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 (file)
index 0000000..21ae841
--- /dev/null
@@ -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<String, String> 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<String> getAllUnitStrings() {
+        return DPT_UNIT_MAP.values().stream();
+    }
+
+    static {
+        // try to get units from Calimeros "unit" field in DPTXlators
+        List<Class<? extends DPTXlator>> translators = List.of(DPTXlator2ByteUnsigned.class, DptXlator2ByteSigned.class,
+                DPTXlator2ByteFloat.class, DPTXlator4ByteUnsigned.class, DPTXlator4ByteSigned.class,
+                DPTXlator4ByteFloat.class, DPTXlator64BitSigned.class);
+
+        for (Class<? extends DPTXlator> 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 (file)
index 0000000..f3c6823
--- /dev/null
@@ -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("^(?<main>[1-9][0-9]{0,2})(?:\\.(?<sub>\\d{3,5}))?$");
+
+    // used to map vendor-specific data to standard DPT
+    public static final Map<String, String> NORMALIZED_DPT = Map.of(//
+            "232.60000", "232.600");
+
+    // fall back if no specific type is defined in DPT_TYPE_MAP
+    private static final Map<String, Set<Class<? extends Type>>> 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<String, Set<Class<? extends Type>>> 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<Class<? extends Type>> getAllowedTypes(String dptId) {
+        Set<Class<? extends Type>> 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 (file)
index 5b2766f..0000000
+++ /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<Integer, Class<? extends Type>> dptMainTypeMap;
-
-    /** stores the openHAB type class for all (supported) KNX datapoint types */
-    private final Map<String, Class<? extends Type>> dptTypeMap;
-
-    /** stores the default KNX DPT to use for each openHAB type */
-    private final Map<Class<? extends Type>, String> defaultDptMap;
-
-    public KNXCoreTypeMapper() {
-        @SuppressWarnings("unused")
-        final List<Class<?>> xlators = Arrays.<Class<?>> 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<? extends Type> 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:<red value> g:<green value> b:<blue value>"
-                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<? extends Type> toTypeClass(@Nullable String dptId) {
-        @Nullable
-        Class<? extends Type> 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<? extends Type> typeClass) {
-        return defaultDptMap.get(typeClass);
-    }
-
-    /**
-     * Formats the given <code>value</code> according to the datapoint type
-     * <code>dpt</code> to a String which can be processed by {@link DateTimeType}.
-     *
-     * @param value
-     * @param dpt
-     *
-     * @return a formatted String like </code>yyyy-MM-dd'T'HH:mm:ss</code> 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 <code>dateType</code> to a knx readable String
-     * according to the target datapoint type <code>dpt</code>.
-     *
-     * @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 (file)
index 0000000..0f1db13
--- /dev/null
@@ -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:(?<r>\\d+) g:(?<g>\\d+) b:(?<b>\\d+)");
+    // RGBW: "100 27 25 12 %", value range: 0-100, invalid values: "-"
+    private static final Pattern RGBW_PATTERN = Pattern
+            .compile("(?:(?<r>[\\d,.]+)|-)\\s(?:(?<g>[\\d,.]+)|-)\\s(?:(?<b>[\\d,.]+)|-)\\s(?:(?<w>[\\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("(?:\\((?<x>\\d+(?:,\\d+)?) (?<y>\\d+(?:,\\d+)?)\\))?\\s*(?:(?<Y>\\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<? extends Type> 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<? extends Type> 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<? extends Type> preferredType)
+            throws KNXFormatException {
+        Set<Class<? extends Type>> 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 (file)
index 0000000..91e102a
--- /dev/null
@@ -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 <code>dateType</code> to a knx readable String
+     * according to the target datapoint type <code>dpt</code>.
+     *
+     * @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();
+    }
+}
index f07593596412f614a417c02ba4a94ab8222fe6ea..b5265982c99e3730411dde69d6b6f72e44f38e77 100644 (file)
@@ -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<String, Object> 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<String, Object> 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 (file)
index fd181d4..0000000
+++ /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<String, String> 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);
-            }
-        }
-    }
-}
index aee86534ab45ac5ac1af239f89fc9fd5b6de0691..8fa423cdcee3b27d5e1000117488768ac6db8b9a 100644 (file)
@@ -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<GroupAddress> groupAddresses = ConcurrentHashMap.newKeySet();
-    private final Set<GroupAddress> groupAddressesWriteBlockedOnce = ConcurrentHashMap.newKeySet();
-    private final Set<OutboundSpec> groupAddressesRespondingSpec = ConcurrentHashMap.newKeySet();
+    private final ExpiringCacheMap<GroupAddress, @Nullable Boolean> groupAddressesWriteBlocked = new ExpiringCacheMap<>(
+            Duration.ofMillis(1000));
+    private final Map<GroupAddress, OutboundSpec> groupAddressesRespondingSpec = new ConcurrentHashMap<>();
     private final Map<GroupAddress, ScheduledFuture<?>> readFutures = new ConcurrentHashMap<>();
     private final Map<ChannelUID, ScheduledFuture<?>> channelFutures = new ConcurrentHashMap<>();
+    private final Map<ChannelUID, KNXChannel> 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<InboundSpec> readSpecs = selector.getReadSpec(configuration);
+    private void scheduleRead(KNXChannel knxChannel) {
+        List<InboundSpec> 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);
-                            }
+            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);
+                        }
+                        if (groupAddressesWriteBlocked.get(destination) != null) {
+                            logger.debug("Write to {} blocked for 1s/one call after read.", destination);
+                            groupAddressesWriteBlocked.invalidate(destination);
                         } else {
-                            logger.debug(
-                                    "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
-                                    channelUID, command, command.getClass().getSimpleName());
+                            getClient().writeToKNX(commandSpec);
                         }
-                    });
-                    break;
+                    } 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<GroupAddress> rsa = getKNXChannelType(channel).getWriteAddresses(channel.getConfiguration());
+    private void sendGroupValueResponse(ChannelUID channelUID, GroupAddress destination) {
+        KNXChannel knxChannel = knxChannels.get(channelUID);
+        if (knxChannel == null) {
+            return;
+        }
+        Set<GroupAddress> rsa = knxChannel.getWriteAddresses();
         if (!rsa.isEmpty()) {
             logger.trace("onGroupRead size '{}'", rsa.size());
-            withKNXType(channel, (selector, configuration) -> {
-                Optional<OutboundSpec> 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));
+        }
+    }
+
+    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();
     }
 
-    private boolean isDPTSupported(@Nullable String dpt) {
-        return typeHelper.toTypeClass(dpt) != null;
+    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<String, String> 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 KNXChannelType getKNXChannelType(Channel channel) {
-        return KNXChannelTypes.getType(channel.getChannelTypeUID());
+    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);
+            }
+        }
     }
 }
index f403aff975387a06bd6dfc856a0b5a682a297c46..e8684e910bd53ef209443f0e30784a4d40ce13dc 100644 (file)
@@ -7,4 +7,12 @@
        <name>KNX Binding</name>
        <description>This binding supports connecting to a KNX bus</description>
 
+       <config-description>
+               <parameter name="disableUoM" type="boolean">
+                       <default>false</default>
+                       <label>Disable UoM</label>
+                       <description>This disables Units of Measurement support for incoming values.</description>
+               </parameter>
+       </config-description>
+
 </addon:addon>
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 (file)
index 0000000..78aa058
--- /dev/null
@@ -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 (file)
index 0000000..866a141
--- /dev/null
@@ -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<GroupAddress> 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<GroupAddress> 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 (file)
index 14bbbb1..0000000
+++ /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<String> 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 (file)
index 0000000..668276f
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index deb5aa5..0000000
+++ /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"));
-    }
-}