/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
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.
## 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
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 |
|-----------|---------------|-------------|
*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"
| 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"
|-----------|---------------|-------------|
| ga | Group address | 9.001 |
+For UoM support see the explanations of the `number` channel.
+
##### Channel Type "string-control"
| Parameter | Description | Default DPT |
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:
*/
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;
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");
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, //
CHANNEL_ROLLERSHUTTER_CONTROL, //
CHANNEL_STRING_CONTROL, //
CHANNEL_SWITCH_CONTROL //
- ).collect(toSet()));
+ );
public static final String CHANNEL_RESET = "reset";
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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());
- }
-}
*/
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;
}
}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
+++ /dev/null
-/**
- * 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();
- }
-}
+++ /dev/null
-/**
- * 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");
- }
-}
*/
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;
*
*/
@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;
}
}
*/
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;
*
*/
@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;
}
}
*/
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;
*
*/
@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);
}
}
*/
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;
*
*/
@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
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;
*
*/
@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
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;
*
*/
@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
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;
*
*/
@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
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
*
*/
@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);
- }
}
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;
*
*/
@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
}
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);
- }
}
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;
*
*/
@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
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;
*
*/
@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
*/
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
*
*/
@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);
+ }
}
*/
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;
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;
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;
@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));
}
};
}
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;
}
}
private synchronized boolean connectIfNotAutomatic() {
if (!isConnected()) {
- return connectJob != null ? false : connect();
+ return connectJob == null && connect();
}
return true;
}
// 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);
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);
}
}
}
- /**
- * 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() {
}
} 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.
}
@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
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;
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
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);
* @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
* @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
* @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);
}
*/
package org.openhab.binding.knx.internal.client;
-import java.util.List;
+import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
*
* @return a list of group addresses.
*/
- List<GroupAddress> getGroupAddresses();
+ Set<GroupAddress> getGroupAddresses();
}
* 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.
}
@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
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;
*
* @return the group address
*/
- @Nullable
GroupAddress getGroupAddress();
/**
*
* @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);
}
/**
* 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);
}
/**
* {@link org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler} configuration
*
- * @author Simon Kaufmann - initial contribution and API
+ * @author Simon Kaufmann - Initial contribution
*
*/
@NonNullByDefault
*/
@NonNullByDefault
public class DeviceConfig {
-
private String address = "";
private boolean fetch = false;
private int pingInterval = 0;
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
+++ /dev/null
-/**
- * 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();
- }
-}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
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;
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;
/**
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
+++ /dev/null
-/**
- * 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);
- }
- }
- }
-}
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;
* 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);
@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) -> {
}
}
- @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()));
}
}
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;
}
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);
}
- });
+ }
}
}
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);
+ }
}
}
}
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);
+ }
+ }
}
}
<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>
--- /dev/null
+/**
+ * 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));
+ }
+}
--- /dev/null
+/**
+ * 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 "";
+ }
+ }
+}
+++ /dev/null
-/**
- * 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 "";
- }
- }
-}
--- /dev/null
+/**
+ * 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));
+ }
+}
+++ /dev/null
-/**
- * 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"));
- }
-}