[knx] Refactoring of KnxCoreTypeMapper and UOM Support (#14534)

* [knx] Refactoring, add basic support for UOM

Preparation for refactoring KnxCoreTypeMapper.
Carryover from smarthomej/addons#107.
Merge UOM implementations.

* [knx] Adapt tests

DPT strings for QuantityType now strip off a tailing .0 when decimals
are converted.

* [knx] Refactoring

Use pattern matching with instanceof operator (new Java17 feature).

* [knx] Refactoring, performance improvements

Introduce KNXChannel class.
Carryover from smarthomej/addons#114.

* [knx] Add warning for incompatible DPT type

Configuring incompatible DPT/channel combinations (e.g. DPT 1.005 (alarm) on Contact channels
or DPT 1.019 (windows/door) on Switch channels) is not allowed but was silently ignored.
This PR adds a warning in case incompatible configurations are detected.

Carryover from smarthomej/addons#203.

* [knx] Add full support for UoM

Replace UoM handling with the implementation from smarthome/j.
Carryover from smarthomej/addons#206.

* [knx] Refactor KNXCoreTypeMapper, add RGBW and xyY

Carryover from smarthomej/addons#208.

* [knx] Fix RGB conversion

Carryover from smarthomej/addons#219.

* [knx] Remove workarounds obsoleted by Calimero 2.5

Carryover from smarthomej/addons#226.

* [knx] Add parameter for disabling incoming UoM

Carryover from smarthomej/addons#230.

* [knx] Fix fallback to DecimalType in number conversion

Carryover from smarthomej/addons#279.

* [knx] Fix DPT 251.600 decoding

Carryover from smarthomej/addons#349.

* [knx] Fix UoM handling for special types
* [knx] Add test for KNXChannelFactory
* [knx] Update CODEOWNERS for knx
* [knx] Default conversion for DPT 5.001 and 6.001
* [knx] Fix write blocked forever after read from bus

Carryover from smarthomej/addons#299 and smarthomej/addons#330.

* [knx] Use new class ColorUtil from core for HSB conversion

Also-by: Jan N. Klug <github@klug.nrw>
Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2023-03-17 12:50:13 +01:00 committed by GitHub
parent b395d0e227
commit aae63e9488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2505 additions and 2757 deletions

View File

@ -159,7 +159,7 @@
/bundles/org.openhab.binding.kaleidescape/ @mlobstein
/bundles/org.openhab.binding.keba/ @kgoderis
/bundles/org.openhab.binding.km200/ @Markinus
/bundles/org.openhab.binding.knx/ @kaikreuzer
/bundles/org.openhab.binding.knx/ @kaikreuzer @holgerfriedrich
/bundles/org.openhab.binding.kodi/ @pail23 @cweitkamp
/bundles/org.openhab.binding.konnected/ @volfan6415
/bundles/org.openhab.binding.kostalinverter/ @cschneider

View File

@ -9,6 +9,18 @@ The KNX binding then can communicate directly with this gateway.
Alternatively, a PC running [KNXD](https://github.com/knxd/knxd) (free open source component software) can be put in between which then acts as a broker allowing multiple client to connect to the same gateway.
Since the protocol is identical, the KNX binding can also communicate with it transparently.
***Attention:*** With the introduction of Unit of Measurement (UoM) support, some data types have changed (see `number` channel below):
- Data type for DPT 5.001 (Percent 8bit, 0 -> 100%) has changed from `PercentType` to `QuantityType`for `number` channels (`dimmer`, `color`, `rollershutter` channels stay with `PercentType`).
- Data type for DPT 5.004 (Percent 8bit, 0 -> 255%) has changed from `PercentType` to `QuantityType`.
- Data type for DPT 6.001 (Percent 8bit -128 -> 127%) has changed from `PercentType` to `QuantityType`.
- Data type for DPT 9.007 (Humidity) has changed from `PercentType` to `QuantityType`.
Rules that check for or compare states and transformations that expect a raw value might need adjustments.
If you run into trouble with that and need some time, you can disable UoM support on binding level via the `disableUoM` parameter.
UoM are enabled by default and need to be disabled manually.
A new setting is activated immediately without restart.
## Supported Things
The KNX binding supports two types of bridges, and one type of things to access the KNX bus.
@ -16,7 +28,8 @@ There is an _ip_ bridge to connect to KNX IP Gateways, and a _serial_ bridge for
## Bridges
The following two bridge types are supported. Bridges don't have channels on their own.
The following two bridge types are supported.
Bridges don't have channels on their own.
### IP Gateway
@ -76,45 +89,30 @@ All channels of a device share one configuration parameter defined on device lev
All readable group addresses are queried by openHAB during startup.
If readInterval is not specified or set to 0, no further periodic reading will be triggered (default: 0).
#### Standard Channel Types
#### Channel Types
Standard channels are used most of the time.
They are used in the common case where the physical state is owned by a device within the KNX bus, e.g. by a switch actuator who "knows" whether the light is turned on or off, or by a temperature sensor which reports the room temperature regularly.
Note: After changing the DPT of already existing Channels, openHAB needs to be restarted for the changes to become effective.
Control channel types (suffix `-control`) are used for cases where the KNX bus does not own the physical state of a device.
This could be the case if e.g. a lamp from another binding should be controlled by a KNX wall switch.
When a `GroupValueRead` telegram is sent from the KNX bus to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus.
##### Channel Type "switch"
| Parameter | Description | Default DPT |
|-----------|-------------------------------------|-------------|
| ga | Group address for the binary switch | 1.001 |
##### Channel Type "dimmer"
##### Channel Type `color`, `color-control`
| Parameter | Description | Default DPT |
|------------------|----------------------------------------|-------------|
| hsb | Group address for the color | 232.600 |
| switch | Group address for the binary switch | 1.001 |
| position | Group address of the absolute position | 5.001 |
| increaseDecrease | Group address for relative movement | 3.007 |
| position | Group address brightness | 5.001 |
| increaseDecrease | Group address for relative brightness | 3.007 |
##### Channel Type "color"
The `hsb` address supports DPT 242.600 and 251.600.
| Parameter | Description | Default DPT |
|------------------|----------------------------------------|-------------|
| hsb | Group address for color | 232.600 |
| switch | Group address for the binary switch | 1.001 |
| position | Group address of the absolute position | 5.001 |
| increaseDecrease | Group address for relative movement | 3.007 |
Some RGB/RGBW products (e.g. MDT) support HSB values for DPT 232.600 instead of RGB.
This is supported as "vendor-specific DPT" with a value of 232.60000.
##### Channel Type "rollershutter"
| Parameter | Description | Default DPT |
|-----------|-----------------------------------------|-------------|
| upDown | Group address for relative movement | 1.008 |
| stopMove | Group address for stopping | 1.010 |
| position | Group address for the absolute position | 5.001 |
##### Channel Type "contact"
##### Channel Type `contact`, `contact-control`
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
@ -123,32 +121,63 @@ Note: After changing the DPT of already existing Channels, openHAB needs to be r
*Attention:* Due to a bug in the original implementation, the states for DPT 1.009 are inverted (i.e. `1` is mapped to `OPEN` instead of `CLOSE`).
A change would break all existing installations and is therefore not implemented.
##### Channel Type "number"
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
| ga | Group address | 9.001 |
Note: Using the Units Of Measurement feature of openHAB (Quantitytype) requires that the DPT value is set correctly.
Automatic type conversion will be applied if required.
##### Channel Type "string"
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
| ga | Group address | 16.001 |
##### Channel Type "datetime"
##### Channel Type `datetime`, `datetime-control`
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
| ga | Group address | 19.001 |
##### Channel Type `dimmer`, `dimmer-control`
| Parameter | Description | Default DPT |
|------------------|----------------------------------------|-------------|
| switch | Group address for the binary switch | 1.001 |
| position | Group address of the absolute position | 5.001 |
| increaseDecrease | Group address for relative movement | 3.007 |
##### Channel Type `number`, `number-control`
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
| ga | Group address | 9.001 |
Note: The `number` channel has full support for Units Of Measurement (UoM).
Using the UoM feature of openHAB (QuantityType) requires that the DPT value is set correctly.
Automatic type conversion will be applied if required.
Incoming values from the KNX bus are converted to values with units (e.g. `23 °C`).
If the channel is linked to the correct item-type (`Number:Temperature` in this case) the display unit can be controlled by item metadata (e.g. `%.1f °F` for 1 digit of precision in Fahrenheit).
The unit is stripped if the channel is linked to a plain number item (type `Number`).
Outgoing values with unit are first converted to the unit associated with the DPT (e.g. a value of `10 °F` is converted to `-8.33 °C` if the channel has DPT 9.001).
Values from plain number channels are sent as-is (without any conversion).
##### Channel Type `rollershutter`, `rollershutter-control`
| Parameter | Description | Default DPT |
|-----------|-----------------------------------------|-------------|
| upDown | Group address for relative movement | 1.008 |
| stopMove | Group address for stopping | 1.010 |
| position | Group address for the absolute position | 5.001 |
##### Channel Type `string`, `string-control`
| Parameter | Description | Default DPT |
|-----------|---------------|-------------|
| ga | Group address | 16.001 |
##### Channel Type `switch`, `switch-control`
| Parameter | Description | Default DPT |
|-----------|-------------------------------------|-------------|
| ga | Group address for the binary switch | 1.001 |
#### Control Channel Types
In contrast to the standard channels above, the control channel types are used for cases where the KNX bus does not own the physical state of a device.
This could for example be the case if a lamp from another binding should be controlled by a KNX wall switch.
If from the KNX bus a `GroupValueRead` telegram is sent to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus.
When a `GroupValueRead` telegram is sent from the KNX bus to a *-control Channel, the bridge responds with a `GroupValueResponse` telegram to the KNX bus.
##### Channel Type "switch-control"
@ -165,14 +194,6 @@ If from the KNX bus a `GroupValueRead` telegram is sent to a *-control Channel,
| increaseDecrease | Group address for relative movement | 3.007 |
| frequency | Increase/Decrease frequency in milliseconds in case the binding should handle that (0 if the KNX device sends the commands repeatedly itself) | 0 |
##### Channel Type "color-control"
| Parameter | Description | Default DPT |
|------------------|----------------------------------------|-------------|
| hsb | Group address for color | 232.600 |
| switch | Group address for the binary switch | 1.001 |
| position | Group address of the absolute position | 5.001 |
| increaseDecrease | Group address for relative movement | 3.007 |
##### Channel Type "rollershutter-control"
@ -197,6 +218,8 @@ A change would break all existing installations and is therefore not implemented
|-----------|---------------|-------------|
| ga | Group address | 9.001 |
For UoM support see the explanations of the `number` channel.
##### Channel Type "string-control"
| Parameter | Description | Default DPT |
@ -340,14 +363,14 @@ Bridge knx:ip:bridge [
knx.items:
```java
Switch demoSwitch "Light [%s]" <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:

View File

@ -12,11 +12,7 @@
*/
package org.openhab.binding.knx.internal;
import static java.util.stream.Collectors.toSet;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@ -32,6 +28,10 @@ public class KNXBindingConstants {
public static final String BINDING_ID = "knx";
// Global config
public static final String CONFIG_DISABLE_UOM = "disableUoM";
public static boolean disableUoM = false;
// Thing Type UIDs
public static final ThingTypeUID THING_TYPE_IP_BRIDGE = new ThingTypeUID(BINDING_ID, "ip");
public static final ThingTypeUID THING_TYPE_SERIAL_BRIDGE = new ThingTypeUID(BINDING_ID, "serial");
@ -84,7 +84,8 @@ public class KNXBindingConstants {
public static final String CHANNEL_SWITCH = "switch";
public static final String CHANNEL_SWITCH_CONTROL = "switch-control";
public static final Set<String> CONTROL_CHANNEL_TYPES = Collections.unmodifiableSet(Stream.of(CHANNEL_COLOR_CONTROL, //
public static final Set<String> CONTROL_CHANNEL_TYPES = Set.of( //
CHANNEL_COLOR_CONTROL, //
CHANNEL_CONTACT_CONTROL, //
CHANNEL_DATETIME_CONTROL, //
CHANNEL_DIMMER_CONTROL, //
@ -92,7 +93,7 @@ public class KNXBindingConstants {
CHANNEL_ROLLERSHUTTER_CONTROL, //
CHANNEL_STRING_CONTROL, //
CHANNEL_SWITCH_CONTROL //
).collect(toSet()));
);
public static final String CHANNEL_RESET = "reset";

View File

@ -1,66 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
/**
* Base class for telegram meta-data
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
public abstract class AbstractSpec {
private String dpt;
protected AbstractSpec(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) {
if (channelConfiguration != null) {
String configuredDPT = channelConfiguration.getDPT();
this.dpt = configuredDPT != null ? configuredDPT : defaultDPT;
} else {
this.dpt = defaultDPT;
}
}
/**
* Helper method to convert a {@link GroupAddressConfiguration} into a {@link GroupAddress}.
*
* @param ga the group address configuration
* @return a group address object
*/
protected final GroupAddress toGroupAddress(GroupAddressConfiguration ga) {
try {
return new GroupAddress(ga.getGA());
} catch (KNXFormatException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Return the data point type.
* <p>
* See {@link org.openhab.binding.knx.internal.client.InboundSpec#getDPT()} and
* {@link org.openhab.binding.knx.internal.client.OutboundSpec#getDPT()}.
*
* @return the data point type.
*/
public final String getDPT() {
return dpt;
}
}

View File

@ -1,58 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.toList;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Data structure representing the content of a channel's group address configuration.
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
public class ChannelConfiguration {
private final @Nullable String dpt;
private final GroupAddressConfiguration mainGA;
private final List<GroupAddressConfiguration> listenGAs;
public ChannelConfiguration(@Nullable String dpt, GroupAddressConfiguration mainGA,
List<GroupAddressConfiguration> listenGAs) {
this.dpt = dpt;
this.mainGA = mainGA;
this.listenGAs = listenGAs;
}
public @Nullable String getDPT() {
return dpt;
}
public GroupAddressConfiguration getMainGA() {
return mainGA;
}
public List<GroupAddressConfiguration> getListenGAs() {
return Stream.concat(Stream.of(mainGA), listenGAs.stream()).collect(toList());
}
public List<GroupAddressConfiguration> getReadGAs() {
return getListenGAs().stream().filter(ga -> ga.isRead()).collect(toList());
}
}

View File

@ -12,41 +12,106 @@
*/
package org.openhab.binding.knx.internal.channel;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
/**
* Data structure representing a single group address configuration within a channel configuration parameter.
* Data structure representing the content of a channel's group address configuration.
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
public class GroupAddressConfiguration {
public static final Logger LOGGER = LoggerFactory.getLogger(GroupAddressConfiguration.class);
private final String ga;
private final boolean read;
private static final Pattern PATTERN_GA_CONFIGURATION = Pattern.compile(
"^((?<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}))");
public GroupAddressConfiguration(String ga, boolean read) {
super();
this.ga = ga;
this.read = read;
private final @Nullable String dpt;
private final GroupAddress mainGA;
private final Set<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;
}
/**
* The group address.
*
* @return the group address.
*/
public String getGA() {
return ga;
public @Nullable String getDPT() {
return dpt;
}
/**
* Denotes whether the group address is marked to be actively read from.
*
* @return {@code true} if read requests should be issued to this address
*/
public boolean isRead() {
return read;
public GroupAddress getMainGA() {
return mainGA;
}
public Set<GroupAddress> getListenGAs() {
return listenGAs;
}
public Set<GroupAddress> getReadGAs() {
return readGAs;
}
public static @Nullable GroupAddressConfiguration parse(@Nullable Object configuration) {
if (!(configuration instanceof String)) {
return null;
}
Matcher matcher = PATTERN_GA_CONFIGURATION.matcher(((String) configuration).replace(" ", ""));
if (matcher.matches()) {
// Listen GAs
String input = matcher.group("listenGAs");
Matcher m2 = PATTERN_LISTEN_GA.matcher(input);
Set<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;
}
}

View File

@ -0,0 +1,154 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.*;
import static org.openhab.binding.knx.internal.KNXBindingConstants.CONTROL_CHANNEL_TYPES;
import static org.openhab.binding.knx.internal.KNXBindingConstants.GA;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.InboundSpec;
import org.openhab.binding.knx.internal.client.OutboundSpec;
import org.openhab.binding.knx.internal.dpt.DPTUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.GroupAddress;
/**
* Meta-data abstraction for the KNX channel configurations.
*
* @author Simon Kaufmann - initial contribution and API
* @author Jan N. Klug - refactored from type definition to channel instance
*
*/
@NonNullByDefault
public abstract class KNXChannel {
private final Logger logger = LoggerFactory.getLogger(KNXChannel.class);
private final Set<String> gaKeys;
private final Map<String, GroupAddressConfiguration> groupAddressConfigurations = new HashMap<>();
private final Set<GroupAddress> listenAddresses = new HashSet<>();
private final Set<GroupAddress> writeAddresses = new HashSet<>();
private final String channelType;
private final ChannelUID channelUID;
private final boolean isControl;
private final Class<? extends Type> preferredType;
KNXChannel(List<Class<? extends Type>> acceptedTypes, Channel channel) {
this(Set.of(GA), acceptedTypes, channel);
}
KNXChannel(Set<String> gaKeys, List<Class<? extends Type>> acceptedTypes, Channel channel) {
this.gaKeys = gaKeys;
this.preferredType = acceptedTypes.get(0);
// this is safe because we already checked the presence of the ChannelTypeUID before
this.channelType = Objects.requireNonNull(channel.getChannelTypeUID()).getId();
this.channelUID = channel.getUID();
this.isControl = CONTROL_CHANNEL_TYPES.contains(channelType);
// build map of ChannelConfigurations and GA lists
Configuration configuration = channel.getConfiguration();
gaKeys.forEach(key -> {
GroupAddressConfiguration groupAddressConfiguration = GroupAddressConfiguration
.parse(configuration.get(key));
if (groupAddressConfiguration != null) {
// check DPT configuration (if set) is compatible with item
String dpt = groupAddressConfiguration.getDPT();
if (dpt != null) {
Set<Class<? extends Type>> types = DPTUtil.getAllowedTypes(dpt);
if (acceptedTypes.stream().noneMatch(types::contains)) {
logger.warn("Configured DPT '{}' is incompatible with accepted types '{}' for channel '{}'",
dpt, acceptedTypes, channelUID);
}
}
groupAddressConfigurations.put(key, groupAddressConfiguration);
// store address configuration for re-use
listenAddresses.addAll(groupAddressConfiguration.getListenGAs());
writeAddresses.add(groupAddressConfiguration.getMainGA());
}
});
}
public String getChannelType() {
return channelType;
}
public ChannelUID getChannelUID() {
return channelUID;
}
public boolean isControl() {
return isControl;
}
public Class<? extends Type> preferredType() {
return preferredType;
}
public final Set<GroupAddress> getAllGroupAddresses() {
return listenAddresses;
}
public final Set<GroupAddress> getWriteAddresses() {
return writeAddresses;
}
public final @Nullable OutboundSpec getCommandSpec(Type command) {
logger.trace("getCommandSpec checking keys '{}' for command '{}' ({})", gaKeys, command, command.getClass());
for (Map.Entry<String, GroupAddressConfiguration> entry : groupAddressConfigurations.entrySet()) {
String dpt = Objects.requireNonNullElse(entry.getValue().getDPT(), getDefaultDPT(entry.getKey()));
Set<Class<? extends Type>> expectedTypeClass = DPTUtil.getAllowedTypes(dpt);
if (expectedTypeClass.contains(command.getClass())) {
logger.trace("getCommandSpec key '{}' has expectedTypeClass '{}', matching command '{}' and dpt '{}'",
entry.getKey(), expectedTypeClass, command, dpt);
return new WriteSpecImpl(entry.getValue(), dpt, command);
}
}
logger.trace("getCommandSpec no Spec found!");
return null;
}
public final List<InboundSpec> getReadSpec() {
return groupAddressConfigurations.entrySet().stream()
.map(entry -> new ReadRequestSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey())))
.filter(spec -> !spec.getGroupAddresses().isEmpty()).collect(toList());
}
public final @Nullable InboundSpec getListenSpec(GroupAddress groupAddress) {
return groupAddressConfigurations.entrySet().stream()
.map(entry -> new ListenSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey())))
.filter(spec -> spec.getGroupAddresses().contains(groupAddress)).findFirst().orElse(null);
}
public final @Nullable OutboundSpec getResponseSpec(GroupAddress groupAddress, Type value) {
return groupAddressConfigurations.entrySet().stream()
.map(entry -> new ReadResponseSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey()), value))
.filter(spec -> spec.matchesDestination(groupAddress)).findFirst().orElse(null);
}
protected abstract String getDefaultDPT(String gaConfigKey);
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* Helper class to find the matching {@link KNXChannel} for any given {@link ChannelTypeUID}.
*
* @author Simon Kaufmann - Initial contribution
* @author Jan N. Klug - Refactored to factory class
*
*/
@NonNullByDefault
public final class KNXChannelFactory {
private static final Map<Set<String>, Function<Channel, KNXChannel>> TYPES = Map.ofEntries( //
Map.entry(TypeColor.SUPPORTED_CHANNEL_TYPES, TypeColor::new), //
Map.entry(TypeContact.SUPPORTED_CHANNEL_TYPES, TypeContact::new), //
Map.entry(TypeDateTime.SUPPORTED_CHANNEL_TYPES, TypeDateTime::new), //
Map.entry(TypeDimmer.SUPPORTED_CHANNEL_TYPES, TypeDimmer::new), //
Map.entry(TypeNumber.SUPPORTED_CHANNEL_TYPES, TypeNumber::new), //
Map.entry(TypeRollershutter.SUPPORTED_CHANNEL_TYPES, TypeRollershutter::new), //
Map.entry(TypeString.SUPPORTED_CHANNEL_TYPES, TypeString::new), //
Map.entry(TypeSwitch.SUPPORTED_CHANNEL_TYPES, TypeSwitch::new));
private KNXChannelFactory() {
// prevent instantiation
}
public static KNXChannel createKnxChannel(Channel channel) throws IllegalArgumentException {
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
throw new IllegalArgumentException("Could not determine ChannelTypeUID for channel " + channel.getUID());
}
String channelType = channelTypeUID.getId();
Function<Channel, KNXChannel> supplier = TYPES.entrySet().stream().filter(e -> e.getKey().contains(channelType))
.map(Map.Entry::getValue).findFirst()
.orElseThrow(() -> new IllegalArgumentException(channelTypeUID + " is not a valid channel type ID"));
return supplier.apply(channel);
}
}

View File

@ -1,218 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.KNXTypeMapper;
import org.openhab.binding.knx.internal.client.InboundSpec;
import org.openhab.binding.knx.internal.client.OutboundSpec;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
/**
* Meta-data abstraction for the KNX channel configurations.
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
public abstract class KNXChannelType {
private static final Pattern PATTERN = Pattern.compile(
"^((?<dpt>[0-9]{1,3}\\.[0-9]{3,4}):)?(?<read>\\<)?(?<mainGA>[0-9]{1,5}(/[0-9]{1,4}){0,2})(?<listenGAs>(\\+(\\<?[0-9]{1,5}(/[0-9]{1,4}){0,2}))*)$");
private static final Pattern PATTERN_LISTEN = Pattern
.compile("\\+((?<read>\\<)?(?<GA>[0-9]{1,5}(/[0-9]{1,4}){0,2}))");
private final Logger logger = LoggerFactory.getLogger(KNXChannelType.class);
private final Set<String> channelTypeIDs;
KNXChannelType(String... channelTypeIDs) {
this.channelTypeIDs = new HashSet<>(Arrays.asList(channelTypeIDs));
}
final Set<String> getChannelIDs() {
return channelTypeIDs;
}
@Nullable
protected final ChannelConfiguration parse(@Nullable String fancy) {
if (fancy == null) {
return null;
}
Matcher matcher = PATTERN.matcher(fancy.replace(" ", ""));
if (matcher.matches()) {
// Listen GAs
String input = matcher.group("listenGAs");
Matcher m2 = PATTERN_LISTEN.matcher(input);
List<GroupAddressConfiguration> listenGAs = new LinkedList<>();
while (m2.find()) {
listenGAs.add(new GroupAddressConfiguration(m2.group("GA"), m2.group("read") != null));
}
// Main GA
GroupAddressConfiguration mainGA = new GroupAddressConfiguration(matcher.group("mainGA"),
matcher.group("read") != null);
return new ChannelConfiguration(matcher.group("dpt"), mainGA, listenGAs);
}
return null;
}
protected abstract Set<String> getAllGAKeys();
public final Set<GroupAddress> getListenAddresses(Configuration channelConfiguration) {
Set<GroupAddress> ret = new HashSet<>();
for (String key : getAllGAKeys()) {
ChannelConfiguration conf = parse((String) channelConfiguration.get(key));
if (conf != null) {
ret.addAll(conf.getListenGAs().stream().map(this::toGroupAddress).collect(toSet()));
}
}
return ret;
}
public final Set<GroupAddress> getReadAddresses(Configuration channelConfiguration) {
Set<GroupAddress> ret = new HashSet<>();
for (String key : getAllGAKeys()) {
ChannelConfiguration conf = parse((String) channelConfiguration.get(key));
if (conf != null) {
ret.addAll(conf.getReadGAs().stream().map(this::toGroupAddress).collect(toSet()));
}
}
return ret;
}
public final Set<GroupAddress> getWriteAddresses(Configuration channelConfiguration) {
Set<GroupAddress> ret = new HashSet<>();
for (String key : getAllGAKeys()) {
ChannelConfiguration conf = parse((String) channelConfiguration.get(key));
if (conf != null) {
GroupAddress ga = toGroupAddress(conf.getMainGA());
if (ga != null) {
ret.add(ga);
}
}
}
return ret;
}
private @Nullable GroupAddress toGroupAddress(GroupAddressConfiguration ga) {
try {
return new GroupAddress(ga.getGA());
} catch (KNXFormatException e) {
logger.warn("Could not parse group address '{}'", ga.getGA());
}
return null;
}
protected final Set<GroupAddress> getAddresses(@Nullable Configuration configuration, Iterable<String> addresses)
throws KNXFormatException {
Set<GroupAddress> ret = new HashSet<>();
for (String address : addresses) {
if (configuration != null && configuration.get(address) != null) {
ret.add(new GroupAddress((String) configuration.get(address)));
}
}
return ret;
}
protected final boolean isEquals(@Nullable Configuration configuration, String address, GroupAddress groupAddress)
throws KNXFormatException {
if (configuration != null && configuration.get(address) != null) {
return Objects.equals(new GroupAddress((String) configuration.get(address)), groupAddress);
}
return false;
}
protected final Set<String> asSet(String... values) {
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(values)));
}
public final @Nullable OutboundSpec getCommandSpec(Configuration configuration, KNXTypeMapper typeHelper,
Type command) throws KNXFormatException {
logger.trace("getCommandSpec testing Keys '{}' for command '{}'", getAllGAKeys(), command);
for (String key : getAllGAKeys()) {
ChannelConfiguration config = parse((String) configuration.get(key));
if (config != null) {
String dpt = config.getDPT();
if (dpt == null) {
dpt = getDefaultDPT(key);
}
Class<? extends Type> expectedTypeClass = typeHelper.toTypeClass(dpt);
if (expectedTypeClass != null) {
if (expectedTypeClass.isInstance(command)
|| ((expectedTypeClass == DecimalType.class) && (command instanceof QuantityType))) {
logger.trace(
"getCommandSpec key '{}' uses expectedTypeClass '{}' which isInstance for command '{}' and dpt '{}'",
key, expectedTypeClass, command, dpt);
return new WriteSpecImpl(config, dpt, command);
}
}
}
}
logger.trace("getCommandSpec no Spec found!");
return null;
}
public final List<InboundSpec> getReadSpec(Configuration configuration) throws KNXFormatException {
return getAllGAKeys().stream()
.map(key -> new ReadRequestSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key)))
.filter(spec -> !spec.getGroupAddresses().isEmpty()).collect(toList());
}
public final @Nullable InboundSpec getListenSpec(Configuration configuration, GroupAddress groupAddress) {
Optional<ListenSpecImpl> result = getAllGAKeys().stream()
.map(key -> new ListenSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key)))
.filter(spec -> !spec.getGroupAddresses().isEmpty())
.filter(spec -> spec.getGroupAddresses().contains(groupAddress)).findFirst();
return result.isPresent() ? result.get() : null;
}
protected abstract String getDefaultDPT(String gaConfigKey);
public final @Nullable OutboundSpec getResponseSpec(Configuration configuration, GroupAddress groupAddress,
Type type) throws KNXFormatException {
Optional<ReadResponseSpecImpl> result = getAllGAKeys().stream()
.map(key -> new ReadResponseSpecImpl(parse((String) configuration.get(key)), getDefaultDPT(key), type))
.filter(spec -> groupAddress.equals(spec.getGroupAddress())).findFirst();
return result.isPresent() ? result.get() : null;
}
@Override
public String toString() {
return channelTypeIDs.toString();
}
}

View File

@ -1,59 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.toSet;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* Helper class to find the matching {@link KNXChannelType} for any given {@link ChannelTypeUID}.
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
public final class KNXChannelTypes {
private static final Set<KNXChannelType> TYPES = Collections.unmodifiableSet(Stream.of(//
new TypeColor(), //
new TypeContact(), //
new TypeDateTime(), //
new TypeDimmer(), //
new TypeNumber(), //
new TypeRollershutter(), //
new TypeString(), //
new TypeSwitch() //
).collect(toSet()));
private KNXChannelTypes() {
// prevent instantiation
}
public static KNXChannelType getType(@Nullable ChannelTypeUID channelTypeUID) throws IllegalArgumentException {
Objects.requireNonNull(channelTypeUID);
for (KNXChannelType c : TYPES) {
if (c.getChannelIDs().contains(channelTypeUID.getId())) {
return c;
}
}
throw new IllegalArgumentException(channelTypeUID.getId() + " is not a valid value channel type ID");
}
}

View File

@ -12,13 +12,10 @@
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.toList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.InboundSpec;
import tuwien.auto.calimero.GroupAddress;
@ -30,21 +27,22 @@ import tuwien.auto.calimero.GroupAddress;
*
*/
@NonNullByDefault
public class ListenSpecImpl extends AbstractSpec implements InboundSpec {
public class ListenSpecImpl implements InboundSpec {
private final String dpt;
private final Set<GroupAddress> listenAddresses;
private final List<GroupAddress> listenAddresses;
public ListenSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) {
super(channelConfiguration, defaultDPT);
if (channelConfiguration != null) {
this.listenAddresses = channelConfiguration.getListenGAs().stream().map(this::toGroupAddress)
.collect(toList());
} else {
this.listenAddresses = Collections.emptyList();
}
public ListenSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) {
this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
this.listenAddresses = groupAddressConfiguration.getListenGAs();
}
public List<GroupAddress> getGroupAddresses() {
@Override
public String getDPT() {
return dpt;
}
@Override
public Set<GroupAddress> getGroupAddresses() {
return listenAddresses;
}
}

View File

@ -12,13 +12,10 @@
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.toList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.InboundSpec;
import tuwien.auto.calimero.GroupAddress;
@ -30,21 +27,22 @@ import tuwien.auto.calimero.GroupAddress;
*
*/
@NonNullByDefault
public class ReadRequestSpecImpl extends AbstractSpec implements InboundSpec {
public class ReadRequestSpecImpl implements InboundSpec {
private final String dpt;
private final Set<GroupAddress> readAddresses;
private final List<GroupAddress> readAddresses;
public ReadRequestSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT) {
super(channelConfiguration, defaultDPT);
if (channelConfiguration != null) {
this.readAddresses = channelConfiguration.getReadGAs().stream().map(this::toGroupAddress).collect(toList());
} else {
this.readAddresses = Collections.emptyList();
}
public ReadRequestSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT) {
this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
this.readAddresses = groupAddressConfiguration.getReadGAs();
}
@Override
public List<GroupAddress> getGroupAddresses() {
public String getDPT() {
return dpt;
}
@Override
public Set<GroupAddress> getGroupAddresses() {
return readAddresses;
}
}

View File

@ -12,8 +12,9 @@
*/
package org.openhab.binding.knx.internal.channel;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.OutboundSpec;
import org.openhab.core.types.Type;
@ -26,28 +27,34 @@ import tuwien.auto.calimero.GroupAddress;
*
*/
@NonNullByDefault
public class ReadResponseSpecImpl extends AbstractSpec implements OutboundSpec {
public class ReadResponseSpecImpl implements OutboundSpec {
private final String dpt;
private final GroupAddress groupAddress;
private final Type value;
private final @Nullable GroupAddress groupAddress;
private final Type type;
public ReadResponseSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT, Type state) {
super(channelConfiguration, defaultDPT);
if (channelConfiguration != null) {
this.groupAddress = toGroupAddress(channelConfiguration.getMainGA());
} else {
this.groupAddress = null;
}
this.type = state;
public ReadResponseSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type state) {
this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
this.groupAddress = groupAddressConfiguration.getMainGA();
this.value = state;
}
@Override
public @Nullable GroupAddress getGroupAddress() {
public String getDPT() {
return dpt;
}
@Override
public GroupAddress getGroupAddress() {
return groupAddress;
}
@Override
public Type getType() {
return type;
public Type getValue() {
return value;
}
@Override
public boolean matchesDestination(GroupAddress groupAddress) {
return groupAddress.equals(this.groupAddress);
}
}

View File

@ -12,13 +12,17 @@
*/
package org.openhab.binding.knx.internal.channel;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
@ -32,15 +36,12 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorRGB;
*
*/
@NonNullByDefault
class TypeColor extends KNXChannelType {
class TypeColor extends KNXChannel {
public static final Set<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

View File

@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
*
*/
@NonNullByDefault
class TypeContact extends KNXChannelType {
class TypeContact extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL);
TypeContact() {
super(CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL);
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.singleton(GA);
TypeContact(Channel channel) {
super(List.of(OpenClosedType.class), channel);
}
@Override

View File

@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
*
*/
@NonNullByDefault
class TypeDateTime extends KNXChannelType {
class TypeDateTime extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL);
TypeDateTime() {
super(CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL);
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.singleton(GA);
TypeDateTime(Channel channel) {
super(List.of(DateTimeType.class), channel);
}
@Override

View File

@ -14,10 +14,15 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
@ -30,15 +35,12 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
*
*/
@NonNullByDefault
class TypeDimmer extends KNXChannelType {
class TypeDimmer extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL);
TypeDimmer() {
super(CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL);
}
@Override
protected Set<String> getAllGAKeys() {
return Set.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA);
TypeDimmer(Channel channel) {
super(Set.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA),
List.of(PercentType.class, OnOffType.class, IncreaseDecreaseType.class), channel);
}
@Override

View File

@ -14,10 +14,13 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Channel;
/**
* number channel type description
@ -26,19 +29,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*
*/
@NonNullByDefault
class TypeNumber extends KNXChannelType {
class TypeNumber extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_NUMBER, CHANNEL_NUMBER_CONTROL);
TypeNumber() {
super(CHANNEL_NUMBER, CHANNEL_NUMBER_CONTROL);
TypeNumber(Channel channel) {
super(List.of(DecimalType.class, QuantityType.class), channel);
}
@Override
protected String getDefaultDPT(String gaConfigKey) {
return "9.001";
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.singleton(GA);
}
}

View File

@ -14,10 +14,15 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
@ -29,10 +34,13 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
*
*/
@NonNullByDefault
class TypeRollershutter extends KNXChannelType {
class TypeRollershutter extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_ROLLERSHUTTER,
CHANNEL_ROLLERSHUTTER_CONTROL);
TypeRollershutter() {
super(CHANNEL_ROLLERSHUTTER, CHANNEL_ROLLERSHUTTER_CONTROL);
TypeRollershutter(Channel channel) {
super(Set.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA),
List.of(PercentType.class, UpDownType.class, StopMoveType.class), channel);
}
@Override
@ -48,9 +56,4 @@ class TypeRollershutter extends KNXChannelType {
}
throw new IllegalArgumentException("GA configuration '" + gaConfigKey + "' is not supported");
}
@Override
protected Set<String> getAllGAKeys() {
return Set.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA);
}
}

View File

@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlatorString;
@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorString;
*
*/
@NonNullByDefault
class TypeString extends KNXChannelType {
class TypeString extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_STRING, CHANNEL_STRING_CONTROL);
TypeString() {
super(CHANNEL_STRING, CHANNEL_STRING_CONTROL);
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.singleton(GA);
TypeString(Channel channel) {
super(List.of(StringType.class), channel);
}
@Override

View File

@ -14,10 +14,12 @@ package org.openhab.binding.knx.internal.channel;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
@ -28,15 +30,11 @@ import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
*
*/
@NonNullByDefault
class TypeSwitch extends KNXChannelType {
class TypeSwitch extends KNXChannel {
public static final Set<String> SUPPORTED_CHANNEL_TYPES = Set.of(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL);
TypeSwitch() {
super(CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL);
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.singleton(GA);
TypeSwitch(Channel channel) {
super(List.of(OnOffType.class), channel);
}
@Override

View File

@ -12,13 +12,13 @@
*/
package org.openhab.binding.knx.internal.channel;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.OutboundSpec;
import org.openhab.core.types.Type;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
/**
* Command meta-data
@ -27,29 +27,34 @@ import tuwien.auto.calimero.KNXFormatException;
*
*/
@NonNullByDefault
public class WriteSpecImpl extends AbstractSpec implements OutboundSpec {
public class WriteSpecImpl implements OutboundSpec {
private final String dpt;
private final Type value;
private final GroupAddress groupAddress;
private final Type type;
private final @Nullable GroupAddress groupAddress;
public WriteSpecImpl(@Nullable ChannelConfiguration channelConfiguration, String defaultDPT, Type type)
throws KNXFormatException {
super(channelConfiguration, defaultDPT);
if (channelConfiguration != null) {
this.groupAddress = new GroupAddress(channelConfiguration.getMainGA().getGA());
} else {
this.groupAddress = null;
}
this.type = type;
public WriteSpecImpl(GroupAddressConfiguration groupAddressConfiguration, String defaultDPT, Type value) {
this.dpt = Objects.requireNonNullElse(groupAddressConfiguration.getDPT(), defaultDPT);
this.groupAddress = groupAddressConfiguration.getMainGA();
this.value = value;
}
@Override
public Type getType() {
return type;
public String getDPT() {
return dpt;
}
@Override
public @Nullable GroupAddress getGroupAddress() {
public Type getValue() {
return value;
}
@Override
public GroupAddress getGroupAddress() {
return groupAddress;
}
@Override
public boolean matchesDestination(GroupAddress groupAddress) {
return groupAddress.equals(this.groupAddress);
}
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.knx.internal.client;
import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
@ -25,8 +27,7 @@ import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.KNXTypeMapper;
import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
import org.openhab.binding.knx.internal.dpt.ValueEncoder;
import org.openhab.binding.knx.internal.handler.GroupAddressListener;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.ThingStatus;
@ -82,7 +83,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private static final int MAX_SEND_ATTEMPTS = 2;
private final Logger logger = LoggerFactory.getLogger(AbstractKNXClient.class);
private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper();
private final ThingUID thingUID;
private final int responseTimeout;
@ -119,23 +119,20 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
@Override
public void groupWrite(ProcessEvent e) {
processEvent("Group Write", e, (listener, source, destination, asdu) -> {
listener.onGroupWrite(AbstractKNXClient.this, source, destination, asdu);
});
processEvent("Group Write", e, (listener, source, destination, asdu) -> listener
.onGroupWrite(AbstractKNXClient.this, source, destination, asdu));
}
@Override
public void groupReadRequest(ProcessEvent e) {
processEvent("Group Read Request", e, (listener, source, destination, asdu) -> {
listener.onGroupRead(AbstractKNXClient.this, source, destination, asdu);
});
processEvent("Group Read Request", e, (listener, source, destination, asdu) -> listener
.onGroupRead(AbstractKNXClient.this, source, destination, asdu));
}
@Override
public void groupReadResponse(ProcessEvent e) {
processEvent("Group Read Response", e, (listener, source, destination, asdu) -> {
listener.onGroupReadResponse(AbstractKNXClient.this, source, destination, asdu);
});
processEvent("Group Read Response", e, (listener, source, destination, asdu) -> listener
.onGroupReadResponse(AbstractKNXClient.this, source, destination, asdu));
}
};
@ -151,21 +148,16 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
public void initialize() {
if (!scheduleReconnectJob()) {
connect();
}
connect();
}
private boolean scheduleReconnectJob() {
private void scheduleReconnectJob() {
if (autoReconnectPeriod > 0) {
// schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec
final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod;
final String prefix = (state == ClientState.INIT) ? "re" : "";
logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS);
connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS);
return true;
} else {
return false;
}
}
@ -181,7 +173,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private synchronized boolean connectIfNotAutomatic() {
if (!isConnected()) {
return connectJob != null ? false : connect();
return connectJob == null && connect();
}
return true;
}
@ -241,15 +233,14 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
// ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero).
// Note for KNX Secure: SAL to be provided
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
this.responseCommunicator = new ProcessCommunicationResponder(link,
new SecureApplicationLayer(link, Security.defaultInstallation()));
this.responseCommunicator = responseCommunicator;
// register this class, callbacks will be triggered
link.addLinkListener(this);
// create a job carrying out read requests
busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause,
busJob = knxScheduler.scheduleWithFixedDelay(this::readNextQueuedDatapoint, 0, readingPause,
TimeUnit.MILLISECONDS);
statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
@ -314,9 +305,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
pc.detach();
});
deviceInfoClient = null;
managementClient = nullify(managementClient, mc -> mc.detach());
managementProcedures = nullify(managementProcedures, mp -> mp.detach());
link = nullify(link, l -> l.close());
managementClient = nullify(managementClient, ManagementClient::detach);
managementProcedures = nullify(managementProcedures, ManagementProcedures::detach);
link = nullify(link, KNXNetworkLink::close);
logger.trace("Bridge {} disconnected from KNX bus", thingUID);
}
@ -339,18 +330,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
}
/**
* Transforms a {@link Type} into a datapoint type value for the KNX bus.
*
* @param type the {@link Type} to transform
* @param dpt the datapoint type to which should be converted
* @return the corresponding KNX datapoint type value as a string
*/
@Nullable
private String toDPTValue(Type type, String dpt) {
return typeHelper.toDPTValue(type, dpt);
}
// datapoint is null at end of the list, warning is misleading
@SuppressWarnings("null")
private void readNextQueuedDatapoint() {
@ -380,7 +359,6 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
} catch (InterruptedException | CancellationException e) {
logger.debug("Interrupted sending KNX read request");
return;
} catch (Exception e) {
// Any other exception: Fail gracefully, i.e. notify user and continue reading next DP.
// Not catching this would end the scheduled read for all DPs in case of an error.
@ -469,13 +447,13 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
@Override
public final boolean registerGroupAddressListener(GroupAddressListener listener) {
return groupAddressListeners.add(listener);
public final void registerGroupAddressListener(GroupAddressListener listener) {
groupAddressListeners.add(listener);
}
@Override
public final boolean unregisterGroupAddressListener(GroupAddressListener listener) {
return groupAddressListeners.remove(listener);
public final void unregisterGroupAddressListener(GroupAddressListener listener) {
groupAddressListeners.remove(listener);
}
@Override
@ -499,7 +477,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
ProcessCommunicator processCommunicator = this.processCommunicator;
KNXNetworkLink link = this.link;
if (processCommunicator == null || link == null) {
logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})",
logger.debug("Cannot write to KNX bus (processCommunicator: {}, link: {})",
processCommunicator == null ? "Not OK" : "OK",
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
return;
@ -508,9 +486,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
logger.trace("writeToKNX groupAddress '{}', commandSpec '{}'", groupAddress, commandSpec);
if (groupAddress != null) {
sendToKNX(processCommunicator, link, groupAddress, commandSpec.getDPT(), commandSpec.getType());
}
sendToKNX(processCommunicator, groupAddress, commandSpec.getDPT(), commandSpec.getValue());
}
@Override
@ -527,27 +503,26 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
logger.trace("respondToKNX groupAddress '{}', responseSpec '{}'", groupAddress, responseSpec);
if (groupAddress != null) {
sendToKNX(responseCommunicator, link, groupAddress, responseSpec.getDPT(), responseSpec.getType());
}
sendToKNX(responseCommunicator, groupAddress, responseSpec.getDPT(), responseSpec.getValue());
}
private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, GroupAddress groupAddress,
String dpt, Type type) throws KNXException {
private void sendToKNX(ProcessCommunication communicator, GroupAddress groupAddress, String dpt, Type type)
throws KNXException {
if (!connectIfNotAutomatic()) {
return;
}
Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0, dpt);
String mappedValue = toDPTValue(type, dpt);
logger.trace("sendToKNX mappedValue: '{}' groupAddress: '{}'", mappedValue, groupAddress);
Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0,
NORMALIZED_DPT.getOrDefault(dpt, dpt));
String mappedValue = ValueEncoder.encode(type, dpt);
if (mappedValue == null) {
logger.debug("Value '{}' cannot be mapped to datapoint '{}'", type, datapoint);
logger.debug("Value '{}' of type '{}' cannot be mapped to datapoint '{}'", type, type.getClass(),
datapoint);
return;
}
for (int i = 0; i < MAX_SEND_ATTEMPTS; i++) {
logger.trace("sendToKNX mappedValue: '{}' groupAddress: '{}'", mappedValue, groupAddress);
for (int i = 0;; i++) {
try {
communicator.write(datapoint, mappedValue);
logger.debug("Wrote value '{}' to datapoint '{}' ({}. attempt).", type, datapoint, i);

View File

@ -33,7 +33,7 @@ public interface BusMessageListener {
* @param destination
* @param asdu
*/
public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu);
void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu);
/**
* Called when the KNX bridge receives a group read telegram
@ -43,7 +43,7 @@ public interface BusMessageListener {
* @param destination
* @param asdu
*/
public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu);
void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu);
/**
* Called when the KNX bridge receives a group read response telegram
@ -53,6 +53,5 @@ public interface BusMessageListener {
* @param destination
* @param asdu
*/
public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
byte[] asdu);
void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu);
}

View File

@ -12,7 +12,7 @@
*/
package org.openhab.binding.knx.internal.client;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -39,5 +39,5 @@ public interface InboundSpec {
*
* @return a list of group addresses.
*/
List<GroupAddress> getGroupAddresses();
Set<GroupAddress> getGroupAddresses();
}

View File

@ -64,17 +64,15 @@ public interface KNXClient {
* Register the given listener to be informed on KNX bus traffic.
*
* @param listener the listener
* @return {@code true} if it wasn't registered before
*/
boolean registerGroupAddressListener(GroupAddressListener listener);
void registerGroupAddressListener(GroupAddressListener listener);
/**
* Remove the given listener.
*
* @param listener the listener
* @return {@code true} if it was successfully removed
*/
boolean unregisterGroupAddressListener(GroupAddressListener listener);
void unregisterGroupAddressListener(GroupAddressListener listener);
/**
* Schedule the given data point for asynchronous reading.

View File

@ -49,13 +49,11 @@ public class NoOpClient implements KNXClient {
}
@Override
public boolean registerGroupAddressListener(GroupAddressListener listener) {
return false;
public void registerGroupAddressListener(GroupAddressListener listener) {
}
@Override
public boolean unregisterGroupAddressListener(GroupAddressListener listener) {
return false;
public void unregisterGroupAddressListener(GroupAddressListener listener) {
}
@Override

View File

@ -13,7 +13,6 @@
package org.openhab.binding.knx.internal.client;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.Type;
import tuwien.auto.calimero.GroupAddress;
@ -39,7 +38,6 @@ public interface OutboundSpec {
*
* @return the group address
*/
@Nullable
GroupAddress getGroupAddress();
/**
@ -47,5 +45,13 @@ public interface OutboundSpec {
*
* @return the command/state
*/
Type getType();
Type getValue();
/**
* Check if group address to be used matches a given group address.
*
* @param groupAddress group address to be compared
* @return true if addresses match
*/
boolean matchesDestination(GroupAddress groupAddress);
}

View File

@ -19,23 +19,29 @@ import org.openhab.core.thing.ThingStatusDetail;
/**
* Callback interface which enables the KNXClient implementations to update the thing status.
*
* @author Simon Kaufmann - initial contribution and API.
* @author Simon Kaufmann - Initial contribution
*
*/
@NonNullByDefault
public interface StatusUpdateCallback {
/**
* see BaseThingHandler
* Updates the status of the thing.
*
* @param status
* see {@link org.openhab.core.thing.binding.BaseThingHandler}
*
* @param status the status
*/
void updateStatus(ThingStatus status);
/**
* see BaseThingHandler
* Updates the status of the thing.
*
* @param status
* see {@link org.openhab.core.thing.binding.BaseThingHandler}
*
* @param status the status
* @param statusDetail the detail of the status
* @param description the description of the status
*/
void updateStatus(ThingStatus status, ThingStatusDetail thingStatusDetail, String message);
void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description);
}

View File

@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler} configuration
*
* @author Simon Kaufmann - initial contribution and API
* @author Simon Kaufmann - Initial contribution
*
*/
@NonNullByDefault

View File

@ -22,7 +22,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class DeviceConfig {
private String address = "";
private boolean fetch = false;
private int pingInterval = 0;

View File

@ -0,0 +1,155 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import tuwien.auto.calimero.dptxlator.DPT;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
import tuwien.auto.calimero.dptxlator.DptXlator2ByteSigned;
/**
* This class provides the units for values depending on the DPT (if available)
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class DPTUnits {
private static final Map<String, String> DPT_UNIT_MAP = new HashMap<>();
private DPTUnits() {
// prevent instantiation
}
/**
* get unit string for a given DPT
*
* @param dptId the KNX DPT
* @return unit string
*/
public static @Nullable String getUnitForDpt(String dptId) {
return DPT_UNIT_MAP.get(dptId);
}
/**
* for testing purposes only
*
* @return stream of all unit strings
*/
static Stream<String> getAllUnitStrings() {
return DPT_UNIT_MAP.values().stream();
}
static {
// try to get units from Calimeros "unit" field in DPTXlators
List<Class<? extends DPTXlator>> translators = List.of(DPTXlator2ByteUnsigned.class, DptXlator2ByteSigned.class,
DPTXlator2ByteFloat.class, DPTXlator4ByteUnsigned.class, DPTXlator4ByteSigned.class,
DPTXlator4ByteFloat.class, DPTXlator64BitSigned.class);
for (Class<? extends DPTXlator> translator : translators) {
Field[] fields = translator.getFields();
for (Field field : fields) {
try {
Object o = field.get(null);
if (o instanceof DPT) {
DPT dpt = (DPT) o;
String unit = dpt.getUnit().replaceAll(" ", "");
// Calimero provides some units (like "ms⁻²") that can't be parsed by our library because of the
// negative exponent
// replace with /
int index = unit.indexOf("");
if (index != -1) {
unit = unit.substring(0, index - 1) + "/" + unit.substring(index - 1).replace("", "");
}
if (!unit.isEmpty()) {
DPT_UNIT_MAP.put(dpt.getID(), unit);
}
}
} catch (IllegalAccessException e) {
// ignore errors
}
}
}
// override/fix units where Calimero data is unparsable or missing
// 8 bit unsigned (DPT 5)
DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_SCALING.getID(), Units.PERCENT.getSymbol()); // required to ensure
// correct conversion
DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_ANGLE.getID(), "°"); // Calimero returns Unicode
DPT_UNIT_MAP.put(DPTXlator8BitUnsigned.DPT_PERCENT_U8.getID(), Units.PERCENT.getSymbol()); // required to ensure
// correct conversion
// 8bit signed (DPT 6)
DPT_UNIT_MAP.put(DPTXlator8BitSigned.DPT_PERCENT_V8.getID(), Units.PERCENT.getSymbol()); // required to ensure
// correct conversion
// two byte unsigned (DPT 7)
DPT_UNIT_MAP.remove(DPTXlator2ByteUnsigned.DPT_VALUE_2_UCOUNT.getID()); // counts have no unit
DPT_UNIT_MAP.put(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_10.getID(), "ms"); // according to spec, it is ms
DPT_UNIT_MAP.put(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_100.getID(), "ms"); // according to spec, it is ms
// two byte signed (DPT 8)
DPT_UNIT_MAP.remove(DptXlator2ByteSigned.DptValueCount.getID()); // pulses habe no unit
// 4 byte unsigned (DPT 12)
DPT_UNIT_MAP.remove(DPTXlator4ByteUnsigned.DPT_VALUE_4_UCOUNT.getID()); // counts have no unit
// 4 byte signed (DPT 13)
DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY.getID(), Units.VAR_HOUR.toString());
DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY_KVARH.getID(), Units.KILOVAR_HOUR.toString());
DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_APPARENT_ENERGY_KVAH.getID(),
Units.KILOVOLT_AMPERE.multiply(Units.HOUR).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteSigned.DPT_FLOWRATE.getID(), Units.CUBICMETRE_PER_HOUR.toString());
DPT_UNIT_MAP.remove(DPTXlator4ByteSigned.DPT_COUNT.getID()); // counts have no unit
// four byte float (DPT 14)
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_CONDUCTANCE.getID(), Units.SIEMENS.toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ANGULAR_MOMENTUM.getID(),
Units.JOULE.multiply(Units.SECOND).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ACTIVITY.getID(), Units.BECQUEREL.toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRICAL_CONDUCTIVITY.getID(),
Units.SIEMENS.divide(SIUnits.METRE).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_TORQUE.getID(), Units.NEWTON.multiply(SIUnits.METRE).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_RESISTIVITY.getID(), Units.OHM.multiply(SIUnits.METRE).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRIC_DIPOLEMOMENT.getID(),
Units.COULOMB.multiply(SIUnits.METRE).toString());
// use definition based on SI units (just rewrite Vm to V*m);
// another common definition uses C, to be handled in encoder
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID(), Units.VOLT.multiply(SIUnits.METRE).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_MAGNETIC_MOMENT.getID(),
Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString());
DPT_UNIT_MAP.put(DPTXlator4ByteFloat.DPT_ELECTROMAGNETIC_MOMENT.getID(),
Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString());
// 64 bit signed (DPT 29)
DPT_UNIT_MAP.put(DPTXlator64BitSigned.DPT_REACTIVE_ENERGY.getID(), Units.VAR_HOUR.toString());
}
}

View File

@ -0,0 +1,127 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
import tuwien.auto.calimero.dptxlator.DPTXlatorString;
/**
* This class provides support to determine compatibility between KNX DPTs and openHAB data types
*
* Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class DPTUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(DPTUtil.class);
// DPT: "123.001", 1-3 digits main type (no leading zero), optional sub-type 3-4 digits (leading zeros allowed)
public static final Pattern DPT_PATTERN = Pattern.compile("^(?<main>[1-9][0-9]{0,2})(?:\\.(?<sub>\\d{3,5}))?$");
// used to map vendor-specific data to standard DPT
public static final Map<String, String> NORMALIZED_DPT = Map.of(//
"232.60000", "232.600");
// fall back if no specific type is defined in DPT_TYPE_MAP
private static final Map<String, Set<Class<? extends Type>>> DPT_MAIN_TYPE_MAP = Map.ofEntries( //
Map.entry("1", Set.of(OnOffType.class)), //
Map.entry("2", Set.of(DecimalType.class)), //
Map.entry("3", Set.of(IncreaseDecreaseType.class)), //
Map.entry("4", Set.of(StringType.class)), //
Map.entry("5", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("6", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("7", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("8", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("9", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("10", Set.of(DateTimeType.class)), //
Map.entry("11", Set.of(DateTimeType.class)), //
Map.entry("12", Set.of(DecimalType.class)), //
Map.entry("13", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("14", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("16", Set.of(StringType.class)), //
Map.entry("17", Set.of(DecimalType.class)), //
Map.entry("18", Set.of(DecimalType.class)), //
Map.entry("19", Set.of(DateTimeType.class)), //
Map.entry("20", Set.of(StringType.class)), //
Map.entry("21", Set.of(StringType.class)), //
Map.entry("22", Set.of(StringType.class)), //
Map.entry("28", Set.of(StringType.class)), //
Map.entry("29", Set.of(QuantityType.class, DecimalType.class)), //
Map.entry("229", Set.of(DecimalType.class)), //
Map.entry("232", Set.of(HSBType.class)), //
Map.entry("242", Set.of(HSBType.class)), //
Map.entry("251", Set.of(HSBType.class, PercentType.class)));
// compatible types for full DPTs
private static final Map<String, Set<Class<? extends Type>>> DPT_TYPE_MAP = Map.ofEntries(
Map.entry(DPTXlatorBoolean.DPT_UPDOWN.getID(), Set.of(UpDownType.class)), //
Map.entry(DPTXlatorBoolean.DPT_OPENCLOSE.getID(), Set.of(OpenClosedType.class)), //
Map.entry(DPTXlatorBoolean.DPT_START.getID(), Set.of(StopMoveType.class)), //
Map.entry(DPTXlatorBoolean.DPT_WINDOW_DOOR.getID(), Set.of(OpenClosedType.class)), //
Map.entry(DPTXlatorBoolean.DPT_SCENE_AB.getID(), Set.of(DecimalType.class)), //
Map.entry(DPTXlator3BitControlled.DPT_CONTROL_BLINDS.getID(), Set.of(UpDownType.class)), //
Map.entry(DPTXlator8BitUnsigned.DPT_SCALING.getID(),
Set.of(QuantityType.class, DecimalType.class, PercentType.class)), //
Map.entry(DPTXlator8BitSigned.DPT_STATUS_MODE3.getID(), Set.of(StringType.class)), //
Map.entry(DPTXlatorString.DPT_STRING_8859_1.getID(), Set.of(StringType.class)), //
Map.entry(DPTXlatorString.DPT_STRING_ASCII.getID(), Set.of(StringType.class)));
private DPTUtil() {
// prevent instantiation
}
/**
* get allowed openHAB types for given DPT
*
* @param dptId the datapoint type id
* @return Set of supported openHAB types (command or state)
*/
public static Set<Class<? extends Type>> getAllowedTypes(String dptId) {
Set<Class<? extends Type>> allowedTypes = DPT_TYPE_MAP.get(dptId);
if (allowedTypes == null) {
Matcher m = DPT_PATTERN.matcher(dptId);
if (!m.matches()) {
LOGGER.warn("getAllowedTypes couldn't identify main number in dptID '{}'", dptId);
return Set.of();
}
allowedTypes = DPT_MAIN_TYPE_MAP.getOrDefault(m.group("main"), Set.of());
}
return allowedTypes;
}
}

View File

@ -0,0 +1,382 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import static org.openhab.binding.knx.internal.KNXBindingConstants.disableUoM;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
import tuwien.auto.calimero.dptxlator.DPTXlatorSceneControl;
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
/**
* This class decodes raw data received from the KNX bus to an openHAB datatype
*
* Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ValueDecoder {
private static final Logger LOGGER = LoggerFactory.getLogger(ValueDecoder.class);
private static final String TIME_DAY_FORMAT = "EEE, HH:mm:ss";
private static final String TIME_FORMAT = "HH:mm:ss";
private static final String DATE_FORMAT = "yyyy-MM-dd";
// RGB: "r:123 g:123 b:123" value-range: 0-255
private static final Pattern RGB_PATTERN = Pattern.compile("r:(?<r>\\d+) g:(?<g>\\d+) b:(?<b>\\d+)");
// RGBW: "100 27 25 12 %", value range: 0-100, invalid values: "-"
private static final Pattern RGBW_PATTERN = Pattern
.compile("(?:(?<r>[\\d,.]+)|-)\\s(?:(?<g>[\\d,.]+)|-)\\s(?:(?<b>[\\d,.]+)|-)\\s(?:(?<w>[\\d,.]+)|-)\\s%");
// xyY: "(0,123 0,123) 56 %", value range 0-1 for xy (comma as decimal point), 0-100 for Y, invalid values omitted
private static final Pattern XYY_PATTERN = Pattern
.compile("(?:\\((?<x>\\d+(?:,\\d+)?) (?<y>\\d+(?:,\\d+)?)\\))?\\s*(?:(?<Y>\\d+(?:,\\d+)?)\\s%)?");
/**
* convert the raw value received to the corresponding openHAB value
*
* @param dptId the DPT of the given data
* @param data a byte array containing the value
* @param preferredType the preferred datatype for this conversion
* @return the data converted to an openHAB Type (or null if conversion failed)
*/
public static @Nullable Type decode(String dptId, byte[] data, Class<? extends Type> preferredType) {
try {
DPTXlator translator = TranslatorTypes.createTranslator(0,
DPTUtil.NORMALIZED_DPT.getOrDefault(dptId, dptId));
translator.setData(data);
String value = translator.getValue();
String id = dptId; // prefer using the user-supplied DPT
Matcher m = DPTUtil.DPT_PATTERN.matcher(id);
if (!m.matches() || m.groupCount() != 2) {
LOGGER.trace("User-Supplied DPT '{}' did not match for sub-type, using DPT returned from Translator",
id);
id = translator.getType().getID();
m = DPTUtil.DPT_PATTERN.matcher(id);
if (!m.matches() || m.groupCount() != 2) {
LOGGER.warn("Couldn't identify main/sub number in dptID '{}'", id);
return null;
}
}
LOGGER.trace("Finally using datapoint DPT = {}", id);
String mainType = m.group("main");
String subType = m.group("sub");
switch (mainType) {
case "1":
return handleDpt1(subType, translator);
case "2":
DPTXlator1BitControlled translator1BitControlled = (DPTXlator1BitControlled) translator;
int decValue = (translator1BitControlled.getControlBit() ? 2 : 0)
+ (translator1BitControlled.getValueBit() ? 1 : 0);
return new DecimalType(decValue);
case "3":
return handleDpt3(subType, translator);
case "10":
return handleDpt10(value);
case "11":
return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN)
.format(new SimpleDateFormat(DATE_FORMAT).parse(value)));
case "18":
DPTXlatorSceneControl translatorSceneControl = (DPTXlatorSceneControl) translator;
int decimalValue = translatorSceneControl.getSceneNumber();
if (value.startsWith("learn")) {
decimalValue += 0x80;
}
return new DecimalType(decimalValue);
case "19":
return handleDpt19(translator);
case "16":
case "20":
case "21":
case "22":
case "28":
return StringType.valueOf(value);
case "232":
return handleDpt232(value, subType);
case "242":
return handleDpt242(value);
case "251":
return handleDpt251(value, preferredType);
default:
return handleNumericDpt(id, translator, preferredType);
}
} catch (NumberFormatException | KNXFormatException | KNXIllegalArgumentException | ParseException e) {
LOGGER.info("Translator couldn't parse data '{}' for datapoint type '{}' ({}).", data, dptId, e.getClass());
} catch (KNXException e) {
LOGGER.warn("Failed creating a translator for datapoint type '{}'.", dptId, e);
}
return null;
}
private static Type handleDpt1(String subType, DPTXlator translator) {
DPTXlatorBoolean translatorBoolean = (DPTXlatorBoolean) translator;
switch (subType) {
case "008":
return translatorBoolean.getValueBoolean() ? UpDownType.DOWN : UpDownType.UP;
case "009":
case "019":
// This is wrong for DPT 1.009. It should be true -> CLOSE, false -> OPEN, but unfortunately
// can't be fixed without breaking a lot of working installations.
// The documentation has been updated to reflect that. / @J-N-K
return translatorBoolean.getValueBoolean() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
case "010":
return translatorBoolean.getValueBoolean() ? StopMoveType.MOVE : StopMoveType.STOP;
case "022":
return DecimalType.valueOf(translatorBoolean.getValueBoolean() ? "1" : "0");
default:
return OnOffType.from(translatorBoolean.getValueBoolean());
}
}
private static @Nullable Type handleDpt3(String subType, DPTXlator translator) {
DPTXlator3BitControlled translator3BitControlled = (DPTXlator3BitControlled) translator;
if (translator3BitControlled.getStepCode() == 0) {
LOGGER.debug("convertRawDataToType: KNX DPT_Control_Dimming: break received.");
return UnDefType.NULL;
}
switch (subType) {
case "007":
return translator3BitControlled.getControlBit() ? IncreaseDecreaseType.INCREASE
: IncreaseDecreaseType.DECREASE;
case "008":
return translator3BitControlled.getControlBit() ? UpDownType.DOWN : UpDownType.UP;
default:
LOGGER.warn("DPT3, subtype '{}' is unknown.", subType);
return null;
}
}
private static Type handleDpt10(String value) throws ParseException {
if (value.contains("no-day")) {
/*
* KNX "no-day" needs special treatment since openHAB's DateTimeType doesn't support "no-day".
* Workaround: remove the "no-day" String, parse the remaining time string, which will result in a
* date of "1970-01-01".
* Replace "no-day" with the current day name
*/
StringBuilder stb = new StringBuilder(value);
int start = stb.indexOf("no-day");
int end = start + "no-day".length();
stb.replace(start, end, String.format(Locale.US, "%1$ta", Calendar.getInstance()));
value = stb.toString();
}
Date date = null;
try {
date = new SimpleDateFormat(TIME_DAY_FORMAT, Locale.US).parse(value);
} catch (ParseException pe) {
date = new SimpleDateFormat(TIME_FORMAT, Locale.US).parse(value);
throw pe;
}
return DateTimeType.valueOf(new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(date));
}
private static @Nullable Type handleDpt19(DPTXlator translator) throws KNXFormatException {
DPTXlatorDateTime translatorDateTime = (DPTXlatorDateTime) translator;
if (translatorDateTime.isFaultyClock()) {
// Not supported: faulty clock
LOGGER.debug("KNX clock msg ignored: clock faulty bit set, which is not supported");
return null;
} else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
// Not supported: "/1/1" (month and day without year)
LOGGER.debug("KNX clock msg ignored: no year, but day and month, which is not supported");
return null;
} else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)) {
// Not supported: "1900" (year without month and day)
LOGGER.debug("KNX clock msg ignored: no day and month, but year, which is not supported");
return null;
} else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& !translatorDateTime.isValidField(DPTXlatorDateTime.DATE)
&& !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
// Not supported: No year, no date and no time
LOGGER.debug("KNX clock msg ignored: no day and month or year, which is not supported");
return null;
}
Calendar cal = Calendar.getInstance();
if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& !translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
// Pure date format, no time information
cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
return DateTimeType.valueOf(value);
} else if (!translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
// Pure time format, no date information
cal.clear();
cal.set(Calendar.HOUR_OF_DAY, translatorDateTime.getHour());
cal.set(Calendar.MINUTE, translatorDateTime.getMinute());
cal.set(Calendar.SECOND, translatorDateTime.getSecond());
String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
return DateTimeType.valueOf(value);
} else if (translatorDateTime.isValidField(DPTXlatorDateTime.YEAR)
&& translatorDateTime.isValidField(DPTXlatorDateTime.TIME)) {
// Date format and time information
cal.setTimeInMillis(translatorDateTime.getValueMilliseconds());
String value = new SimpleDateFormat(DateTimeType.DATE_PATTERN).format(cal.getTime());
return DateTimeType.valueOf(value);
} else {
LOGGER.warn("Failed to convert '{}'", translator.getValue());
return null;
}
}
private static @Nullable Type handleDpt232(String value, String subType) {
Matcher rgb = RGB_PATTERN.matcher(value);
if (rgb.matches()) {
int r = Integer.parseInt(rgb.group("r"));
int g = Integer.parseInt(rgb.group("g"));
int b = Integer.parseInt(rgb.group("b"));
switch (subType) {
case "600":
return HSBType.fromRGB(r, g, b);
case "60000":
// MDT specific: mis-use 232.600 for hsv instead of rgb
DecimalType hue = new DecimalType(coerceToRange(r * 360.0 / 255.0, 0.0, 359.9999));
PercentType sat = new PercentType(BigDecimal.valueOf(coerceToRange(g / 2.55, 0.0, 100.0)));
PercentType bright = new PercentType(BigDecimal.valueOf(coerceToRange(b / 2.55, 0.0, 100.0)));
return new HSBType(hue, sat, bright);
default:
LOGGER.warn("Unknown subtype '232.{}', no conversion possible.", subType);
return null;
}
}
LOGGER.warn("Failed to convert '{}' (DPT 232): Pattern does not match", value);
return null;
}
private static @Nullable Type handleDpt242(String value) {
Matcher xyY = XYY_PATTERN.matcher(value);
if (xyY.matches()) {
String stringx = xyY.group("x");
String stringy = xyY.group("y");
String stringY = xyY.group("Y");
if (stringx != null && stringy != null) {
double x = Double.parseDouble(stringx.replace(",", "."));
double y = Double.parseDouble(stringy.replace(",", "."));
if (stringY == null) {
return ColorUtil.xyToHsv(new double[] { x, y });
} else {
double Y = Double.parseDouble(stringY.replace(",", "."));
return ColorUtil.xyToHsv(new double[] { x, y, Y });
}
}
}
LOGGER.warn("Failed to convert '{}' (DPT 242): Pattern does not match", value);
return null;
}
private static @Nullable Type handleDpt251(String value, Class<? extends Type> preferredType) {
Matcher rgbw = RGBW_PATTERN.matcher(value);
if (rgbw.matches()) {
String rString = rgbw.group("r");
String gString = rgbw.group("g");
String bString = rgbw.group("b");
String wString = rgbw.group("w");
if (rString != null && gString != null && bString != null && HSBType.class.equals(preferredType)) {
// does not support PercentType and r,g,b valid -> HSBType
int r = coerceToRange((int) (Double.parseDouble(rString.replace(",", ".")) * 2.55), 0, 255);
int g = coerceToRange((int) (Double.parseDouble(gString.replace(",", ".")) * 2.55), 0, 255);
int b = coerceToRange((int) (Double.parseDouble(bString.replace(",", ".")) * 2.55), 0, 255);
return HSBType.fromRGB(r, g, b);
} else if (wString != null && PercentType.class.equals(preferredType)) {
// does support PercentType and w valid -> PercentType
BigDecimal w = new BigDecimal(wString.replace(",", "."));
return new PercentType(w);
}
}
LOGGER.warn("Failed to convert '{}' (DPT 251): Pattern does not match or invalid content", value);
return null;
}
private static @Nullable Type handleNumericDpt(String id, DPTXlator translator, Class<? extends Type> preferredType)
throws KNXFormatException {
Set<Class<? extends Type>> allowedTypes = DPTUtil.getAllowedTypes(id);
double value = translator.getNumericValue();
if (allowedTypes.contains(PercentType.class)
&& (HSBType.class.equals(preferredType) || PercentType.class.equals(preferredType))) {
return new PercentType(BigDecimal.valueOf(Math.round(value)));
}
if (allowedTypes.contains(QuantityType.class) && !disableUoM) {
String unit = DPTUnits.getUnitForDpt(id);
if (unit != null) {
return new QuantityType<>(value + " " + unit);
} else {
LOGGER.trace("Could not determine unit for DPT '{}', fallback to plain decimal", id);
}
}
if (allowedTypes.contains(DecimalType.class)) {
return new DecimalType(value);
}
LOGGER.warn("Failed to convert '{}' (DPT '{}'): no matching type found", value, id);
return null;
}
private static double coerceToRange(double value, double min, double max) {
return Math.min(Math.max(value, min), max);
}
private static int coerceToRange(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
}

View File

@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import static org.openhab.binding.knx.internal.dpt.DPTUtil.NORMALIZED_DPT;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;
import java.util.regex.Matcher;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.Type;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.dptxlator.DPT;
import tuwien.auto.calimero.dptxlator.DPTXlator;
import tuwien.auto.calimero.dptxlator.DPTXlator1BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlator3BitControlled;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlatorDate;
import tuwien.auto.calimero.dptxlator.DPTXlatorDateTime;
import tuwien.auto.calimero.dptxlator.DPTXlatorTime;
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
/**
* This class encodes openHAB data types to strings for sending via Calimero
*
* Parts of this code are based on the openHAB KNXCoreTypeMapper by Kai Kreuzer et al.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ValueEncoder {
private static final Logger LOGGER = LoggerFactory.getLogger(ValueEncoder.class);
private ValueEncoder() {
// prevent instantiation
}
/**
* Formats the given value as String for outputting via Calimero.
*
* @param value the value
* @param dptId the DPT id to use for formatting the string (e.g. 9.001)
* @return the value formatted as String
*/
public static @Nullable String encode(Type value, String dptId) {
Matcher m = DPTUtil.DPT_PATTERN.matcher(dptId);
if (!m.matches() || m.groupCount() != 2) {
LOGGER.warn("Couldn't identify main/sub number in dptId '{}'", dptId);
return null;
}
String mainNumber = m.group("main");
try {
DPTXlator translator = TranslatorTypes.createTranslator(Integer.parseInt(mainNumber),
NORMALIZED_DPT.getOrDefault(dptId, dptId));
DPT dpt = translator.getType();
// check for HSBType first, because it extends PercentType as well
if (value instanceof HSBType) {
return handleHSBType(dptId, (HSBType) value);
} else if (value instanceof OnOffType) {
return OnOffType.OFF.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue();
} else if (value instanceof UpDownType) {
return UpDownType.UP.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue();
} else if (value instanceof IncreaseDecreaseType) {
DPT valueDPT = ((DPTXlator3BitControlled.DPT3BitControlled) dpt).getControlDPT();
return IncreaseDecreaseType.DECREASE.equals(value) ? valueDPT.getLowerValue() + " 5"
: valueDPT.getUpperValue() + " 5";
} else if (value instanceof OpenClosedType) {
return OpenClosedType.CLOSED.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue();
} else if (value instanceof StopMoveType) {
return StopMoveType.STOP.equals(value) ? dpt.getLowerValue() : dpt.getUpperValue();
} else if (value instanceof PercentType) {
int intValue = ((PercentType) value).intValue();
return "251.600".equals(dptId) ? String.format("- - - %d %%", intValue) : String.valueOf(intValue);
} else if (value instanceof DecimalType || value instanceof QuantityType<?>) {
return handleNumericTypes(dptId, mainNumber, dpt, value);
} else if (value instanceof StringType) {
return value.toString();
} else if (value instanceof DateTimeType) {
return handleDateTimeType(dptId, (DateTimeType) value);
}
} catch (KNXException e) {
return null;
} catch (Exception e) {
LOGGER.warn("An exception occurred converting value {} to dpt id {}: error message={}", value, dptId,
e.getMessage());
return null;
}
LOGGER.debug("formatAsDPTString: Couldn't convert value {} to dpt id {} (no mapping).", value, dptId);
return null;
}
/**
* Formats the given internal <code>dateType</code> to a knx readable String
* according to the target datapoint type <code>dpt</code>.
*
* @param value the input value
* @param dptId the target datapoint type
*
* @return a String which contains either an ISO8601 formatted date (yyyy-mm-dd),
* a formatted 24-hour clock with the day of week prepended (Mon, 12:00:00) or
* a formatted 24-hour clock (12:00:00)
*/
private static @Nullable String handleDateTimeType(String dptId, DateTimeType value) {
if (DPTXlatorDate.DPT_DATE.getID().equals(dptId)) {
return value.format("%tF");
} else if (DPTXlatorTime.DPT_TIMEOFDAY.getID().equals(dptId)) {
return value.format(Locale.US, "%1$ta, %1$tT");
} else if (DPTXlatorDateTime.DPT_DATE_TIME.getID().equals(dptId)) {
return value.format(Locale.US, "%tF %1$tT");
}
LOGGER.warn("Could not format DateTimeType for datapoint type '{}'", dptId);
return null;
}
private static String handleHSBType(String dptId, HSBType hsb) {
switch (dptId) {
case "232.600":
return "r:" + convertPercentToByte(hsb.getRed()) + " g:" + convertPercentToByte(hsb.getGreen()) + " b:"
+ convertPercentToByte(hsb.getBlue());
case "232.60000":
// MDT specific: mis-use 232.600 for hsv instead of rgb
int hue = hsb.getHue().toBigDecimal().multiply(BigDecimal.valueOf(255))
.divide(BigDecimal.valueOf(360), 2, RoundingMode.HALF_UP).intValue();
return "r:" + hue + " g:" + convertPercentToByte(hsb.getSaturation()) + " b:"
+ convertPercentToByte(hsb.getBrightness());
case "242.600":
double[] xyY = ColorUtil.hsbToXY(hsb);
return String.format("(%,.4f %,.4f) %,.1f %%", xyY[0], xyY[1], xyY[2] * 100.0);
case "251.600":
return String.format("%d %d %d - %%", hsb.getRed().intValue(), hsb.getGreen().intValue(),
hsb.getBlue().intValue());
case "5.003":
return hsb.getHue().toString();
default:
return hsb.getBrightness().toString();
}
}
private static String handleNumericTypes(String dptId, String mainNumber, DPT dpt, Type value) {
BigDecimal bigDecimal;
if (value instanceof DecimalType decimalType) {
bigDecimal = decimalType.toBigDecimal();
} else {
String unit = DPTUnits.getUnitForDpt(dptId);
// exception for DPT using temperature differences
// - conversion °C or °F to K is wrong for differences,
// - stick to the unit given, fix the scaling for °F
// 9.002 DPT_Value_Tempd
// 9.003 DPT_Value_Tempa
// 9.023 DPT_KelvinPerPercent
if (DPTXlator2ByteFloat.DPT_TEMPERATURE_DIFFERENCE.getID().equals(dptId)
|| DPTXlator2ByteFloat.DPT_TEMPERATURE_GRADIENT.getID().equals(dptId)
|| DPTXlator2ByteFloat.DPT_KELVIN_PER_PERCENT.getID().equals(dptId)) {
// match unicode character or °C
if (value.toString().contains(SIUnits.CELSIUS.getSymbol()) || value.toString().contains("°C")) {
unit = unit.replace("K", "°C");
} else if (value.toString().contains("°F")) {
unit = unit.replace("K", "°F");
value = ((QuantityType<?>) value).multiply(BigDecimal.valueOf(5.0 / 9.0));
}
} else if (DPTXlator4ByteFloat.DPT_LIGHT_QUANTITY.getID().equals(dptId)) {
if (!value.toString().contains("J")) {
unit = unit.replace("J", "lm*s");
}
} else if (DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getID().equals(dptId)) {
// use alternate definition of flux
if (value.toString().contains("C")) {
unit = "C";
}
}
if (unit != null) {
QuantityType<?> converted = ((QuantityType<?>) value).toUnit(unit);
if (converted == null) {
LOGGER.warn("Could not convert {} to unit {}, stripping unit only. Check your configuration.",
value, unit);
bigDecimal = ((QuantityType<?>) value).toBigDecimal();
} else {
bigDecimal = converted.toBigDecimal();
}
} else {
bigDecimal = ((QuantityType<?>) value).toBigDecimal();
}
}
switch (mainNumber) {
case "2":
DPT valueDPT = ((DPTXlator1BitControlled.DPT1BitControlled) dpt).getValueDPT();
switch (bigDecimal.intValue()) {
case 0:
return "0 " + valueDPT.getLowerValue();
case 1:
return "0 " + valueDPT.getUpperValue();
case 2:
return "1 " + valueDPT.getLowerValue();
default:
return "1 " + valueDPT.getUpperValue();
}
case "18":
int intVal = bigDecimal.intValue();
if (intVal > 63) {
return "learn " + (intVal - 0x80);
} else {
return "activate " + intVal;
}
default:
return bigDecimal.stripTrailingZeros().toPlainString();
}
}
/**
* convert 0...100% to 1 byte 0..255
*
* @param percent
* @return int 0..255
*/
private static int convertPercentToByte(PercentType percent) {
return percent.toBigDecimal().multiply(BigDecimal.valueOf(255))
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).intValue();
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.knx.internal.factory;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -38,6 +39,7 @@ import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
/**
@ -54,17 +56,23 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {
THING_TYPE_IP_BRIDGE, THING_TYPE_SERIAL_BRIDGE);
@Nullable
private NetworkAddressService networkAddressService;
private final NetworkAddressService networkAddressService;
private final SerialPortManager serialPortManager;
@Activate
public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService,
public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map<String, Object> config,
final @Reference TranslationProvider translationProvider, final @Reference LocaleProvider localeProvider,
final @Reference SerialPortManager serialPortManager) {
KNXTranslationProvider.I18N.setProvider(localeProvider, translationProvider);
this.networkAddressService = networkAddressService;
this.serialPortManager = serialPortManager;
SerialTransportAdapter.setSerialPortManager(serialPortManager);
modified(config);
}
@Modified
protected void modified(Map<String, Object> config) {
disableUoM = (boolean) config.getOrDefault(CONFIG_DISABLE_UOM, false);
}
@Override

View File

@ -1,226 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.handler;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.DeviceInspector;
import org.openhab.binding.knx.internal.client.DeviceInspector.Result;
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.config.DeviceConfig;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException;
/**
* Base class for KNX thing handlers.
*
* @author Simon Kaufmann - initial contribution and API
*
*/
@NonNullByDefault
public abstract class AbstractKNXThingHandler extends BaseThingHandler implements GroupAddressListener {
private static final int INITIAL_PING_DELAY = 5;
private final Logger logger = LoggerFactory.getLogger(AbstractKNXThingHandler.class);
protected @Nullable IndividualAddress address;
private @Nullable ScheduledFuture<?> descriptionJob;
private boolean filledDescription = false;
private final Random random = new Random();
private @Nullable ScheduledFuture<?> pollingJob;
public AbstractKNXThingHandler(Thing thing) {
super(thing);
}
protected final ScheduledExecutorService getScheduler() {
return getBridgeHandler().getScheduler();
}
protected final ScheduledExecutorService getBackgroundScheduler() {
return getBridgeHandler().getBackgroundScheduler();
}
protected final KNXBridgeBaseThingHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler();
if (handler != null) {
return handler;
}
}
throw new IllegalStateException("The bridge must not be null and must be initialized");
}
protected final KNXClient getClient() {
return getBridgeHandler().getClient();
}
protected final boolean describeDevice(@Nullable IndividualAddress address) {
if (address == null) {
return false;
}
DeviceInspector inspector = new DeviceInspector(getClient().getDeviceInfoClient(), address);
Result result = inspector.readDeviceInfo();
if (result != null) {
Map<String, String> properties = editProperties();
properties.putAll(result.getProperties());
updateProperties(properties);
return true;
}
return false;
}
protected final String asduToHex(byte[] asdu) {
final char[] hexCode = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder(2 + asdu.length * 2);
sb.append("0x");
for (byte b : asdu) {
sb.append(hexCode[(b >> 4) & 0xF]);
sb.append(hexCode[(b & 0xF)]);
}
return sb.toString();
}
protected final void restart() {
if (address != null) {
getClient().restartNetworkDevice(address);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
attachToClient();
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
detachFromClient();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void initialize() {
attachToClient();
}
@Override
public void dispose() {
detachFromClient();
}
protected abstract void scheduleReadJobs();
protected abstract void cancelReadFutures();
private void pollDeviceStatus() {
try {
if (address != null && getClient().isConnected()) {
logger.debug("Polling individual address '{}'", address);
boolean isReachable = getClient().isReachable(address);
if (isReachable) {
updateStatus(ThingStatus.ONLINE);
DeviceConfig config = getConfigAs(DeviceConfig.class);
if (!filledDescription && config.getFetch()) {
Future<?> descriptionJob = this.descriptionJob;
if (descriptionJob == null || descriptionJob.isCancelled()) {
long initialDelay = Math.round(config.getPingInterval() * random.nextFloat());
this.descriptionJob = getBackgroundScheduler().schedule(() -> {
filledDescription = describeDevice(address);
}, initialDelay, TimeUnit.SECONDS);
}
}
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
} catch (KNXException e) {
logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
KNXTranslationProvider.I18N.getLocalizedException(e));
}
}
protected void attachToClient() {
if (!getClient().isConnected()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
DeviceConfig config = getConfigAs(DeviceConfig.class);
try {
if (!config.getAddress().isEmpty()) {
updateStatus(ThingStatus.UNKNOWN);
address = new IndividualAddress(config.getAddress());
long pingInterval = config.getPingInterval();
long initialPingDelay = Math.round(INITIAL_PING_DELAY * random.nextFloat());
ScheduledFuture<?> pollingJob = this.pollingJob;
if ((pollingJob == null || pollingJob.isCancelled())) {
logger.debug("'{}' will be polled every {}s", getThing().getUID(), pingInterval);
this.pollingJob = getBackgroundScheduler().scheduleWithFixedDelay(() -> pollDeviceStatus(),
initialPingDelay, pingInterval, TimeUnit.SECONDS);
}
} else {
updateStatus(ThingStatus.ONLINE);
}
} catch (KNXFormatException e) {
logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
KNXTranslationProvider.I18N.getLocalizedException(e));
}
getClient().registerGroupAddressListener(this);
scheduleReadJobs();
}
protected void detachFromClient() {
final var pollingJobSynced = pollingJob;
if (pollingJobSynced != null) {
pollingJobSynced.cancel(true);
pollingJob = null;
}
final var descriptionJobSynced = descriptionJob;
if (descriptionJobSynced != null) {
descriptionJobSynced.cancel(true);
descriptionJob = null;
}
cancelReadFutures();
Bridge bridge = getBridge();
if (bridge != null) {
KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler();
if (handler != null) {
handler.getClient().unregisterGroupAddressListener(this);
}
}
}
}

View File

@ -15,37 +15,48 @@ package org.openhab.binding.knx.internal.handler;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.KNXBindingConstants;
import org.openhab.binding.knx.internal.KNXTypeMapper;
import org.openhab.binding.knx.internal.channel.KNXChannelType;
import org.openhab.binding.knx.internal.channel.KNXChannelTypes;
import org.openhab.binding.knx.internal.channel.KNXChannel;
import org.openhab.binding.knx.internal.channel.KNXChannelFactory;
import org.openhab.binding.knx.internal.client.AbstractKNXClient;
import org.openhab.binding.knx.internal.client.DeviceInspector;
import org.openhab.binding.knx.internal.client.InboundSpec;
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.client.OutboundSpec;
import org.openhab.binding.knx.internal.config.DeviceConfig;
import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
import org.openhab.core.config.core.Configuration;
import org.openhab.binding.knx.internal.dpt.DPTUtil;
import org.openhab.binding.knx.internal.dpt.ValueDecoder;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -61,19 +72,26 @@ import tuwien.auto.calimero.datapoint.Datapoint;
* bus and updating the channels correspondingly.
*
* @author Simon Kaufmann - Initial contribution and API
* @author Jan N. Klug - Refactored for performance
*/
@NonNullByDefault
public class DeviceThingHandler extends AbstractKNXThingHandler {
public class DeviceThingHandler extends BaseThingHandler implements GroupAddressListener {
private static final int INITIAL_PING_DELAY = 5;
private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class);
private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper();
private final Set<GroupAddress> groupAddresses = ConcurrentHashMap.newKeySet();
private final Set<GroupAddress> groupAddressesWriteBlockedOnce = ConcurrentHashMap.newKeySet();
private final Set<OutboundSpec> groupAddressesRespondingSpec = ConcurrentHashMap.newKeySet();
private final ExpiringCacheMap<GroupAddress, @Nullable Boolean> groupAddressesWriteBlocked = new ExpiringCacheMap<>(
Duration.ofMillis(1000));
private final Map<GroupAddress, OutboundSpec> groupAddressesRespondingSpec = new ConcurrentHashMap<>();
private final Map<GroupAddress, ScheduledFuture<?>> readFutures = new ConcurrentHashMap<>();
private final Map<ChannelUID, ScheduledFuture<?>> channelFutures = new ConcurrentHashMap<>();
private final Map<ChannelUID, KNXChannel> knxChannels = new ConcurrentHashMap<>();
private final Random random = new Random();
protected @Nullable IndividualAddress address;
private int readInterval;
private @Nullable ScheduledFuture<?> descriptionJob;
private boolean filledDescription = false;
private @Nullable ScheduledFuture<?> pollingJob;
public DeviceThingHandler(Thing thing) {
super(thing);
@ -81,43 +99,34 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
@Override
public void initialize() {
super.initialize();
attachToClient();
DeviceConfig config = getConfigAs(DeviceConfig.class);
readInterval = config.getReadInterval();
initializeGroupAddresses();
}
private void initializeGroupAddresses() {
forAllChannels((selector, channelConfiguration) -> {
groupAddresses.addAll(selector.getReadAddresses(channelConfiguration));
groupAddresses.addAll(selector.getWriteAddresses(channelConfiguration));
groupAddresses.addAll(selector.getListenAddresses(channelConfiguration));
// gather all GAs from channel configurations and create channels
getThing().getChannels().forEach(channel -> {
KNXChannel knxChannel = KNXChannelFactory.createKnxChannel(channel);
knxChannels.put(channel.getUID(), knxChannel);
groupAddresses.addAll(knxChannel.getAllGroupAddresses());
});
}
@Override
public void dispose() {
cancelChannelFutures();
freeGroupAddresses();
super.dispose();
}
private void cancelChannelFutures() {
for (ChannelUID channelUID : channelFutures.keySet()) {
channelFutures.computeIfPresent(channelUID, (k, v) -> {
v.cancel(true);
return null;
});
}
}
private void freeGroupAddresses() {
groupAddresses.clear();
groupAddressesWriteBlockedOnce.clear();
groupAddressesWriteBlocked.clear();
groupAddressesRespondingSpec.clear();
knxChannels.clear();
detachFromClient();
}
@Override
protected void cancelReadFutures() {
for (GroupAddress groupAddress : readFutures.keySet()) {
readFutures.computeIfPresent(groupAddress, (k, v) -> {
@ -127,62 +136,31 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
}
}
@FunctionalInterface
private interface ChannelFunction {
void apply(KNXChannelType channelType, Configuration configuration) throws KNXException;
}
private void withKNXType(ChannelUID channelUID, ChannelFunction function) {
Channel channel = getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.warn("Channel '{}' does not exist", channelUID);
return;
}
withKNXType(channel, function);
}
private void withKNXType(Channel channel, ChannelFunction function) {
try {
KNXChannelType selector = getKNXChannelType(channel);
function.apply(selector, channel.getConfiguration());
} catch (KNXException e) {
logger.warn("An error occurred on channel {}: {}", channel.getUID(), e.getMessage(), e);
}
}
private void forAllChannels(ChannelFunction function) {
for (Channel channel : getThing().getChannels()) {
withKNXType(channel, function);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
if (!isControl(channelUID)) {
withKNXType(channelUID, (selector, configuration) -> {
scheduleRead(selector, configuration);
});
KNXChannel knxChannel = knxChannels.get(channelUID);
if (knxChannel == null) {
logger.warn("Channel '{}' received a channel linked event, but no KNXChannel found", channelUID);
return;
}
if (!knxChannel.isControl()) {
scheduleRead(knxChannel);
}
}
@Override
protected void scheduleReadJobs() {
cancelReadFutures();
for (Channel channel : getThing().getChannels()) {
if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) {
withKNXType(channel, (selector, configuration) -> {
scheduleRead(selector, configuration);
});
for (KNXChannel knxChannel : knxChannels.values()) {
if (isLinked(knxChannel.getChannelUID()) && !knxChannel.isControl()) {
scheduleRead(knxChannel);
}
}
}
private void scheduleRead(KNXChannelType selector, Configuration configuration) throws KNXFormatException {
List<InboundSpec> readSpecs = selector.getReadSpec(configuration);
private void scheduleRead(KNXChannel knxChannel) {
List<InboundSpec> readSpecs = knxChannel.getReadSpec();
for (InboundSpec readSpec : readSpecs) {
for (GroupAddress groupAddress : readSpec.getGroupAddresses()) {
scheduleReadJob(groupAddress, readSpec.getDPT());
}
readSpec.getGroupAddresses().forEach(ga -> scheduleReadJob(ga, readSpec.getDPT()));
}
}
@ -201,7 +179,7 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
private void readDatapoint(GroupAddress groupAddress, String dpt) {
if (getClient().isConnected()) {
if (!isDPTSupported(dpt)) {
if (DPTUtil.getAllowedTypes(dpt).isEmpty()) {
logger.warn("DPT '{}' is not supported by the KNX binding", dpt);
return;
}
@ -215,89 +193,71 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
return groupAddresses.contains(destination);
}
/** KNXIO remember controls, removeIf may be null */
@SuppressWarnings("null")
private void rememberRespondingSpec(OutboundSpec commandSpec, boolean add) {
GroupAddress ga = commandSpec.getGroupAddress();
if (ga != null) {
groupAddressesRespondingSpec.removeIf(spec -> spec.getGroupAddress().equals(ga));
}
if (add) {
groupAddressesRespondingSpec.add(commandSpec);
}
logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga,
groupAddressesRespondingSpec.size(), add);
}
/** Handling commands triggered from openHAB */
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Handling command '{}' for channel '{}'", command, channelUID);
if (command instanceof RefreshType && !isControl(channelUID)) {
KNXChannel knxChannel = knxChannels.get(channelUID);
if (knxChannel == null) {
logger.warn("Channel '{}' received command, but no KNXChannel found", channelUID);
return;
}
if (command instanceof RefreshType && !knxChannel.isControl()) {
logger.debug("Refreshing channel '{}'", channelUID);
withKNXType(channelUID, (selector, configuration) -> {
scheduleRead(selector, configuration);
});
scheduleRead(knxChannel);
} else {
switch (channelUID.getId()) {
case CHANNEL_RESET:
if (address != null) {
restart();
}
break;
default:
withKNXType(channelUID, (selector, channelConfiguration) -> {
OutboundSpec commandSpec = selector.getCommandSpec(channelConfiguration, typeHelper, command);
// only send GroupValueWrite to KNX if GA is not blocked once
if (commandSpec != null
&& !groupAddressesWriteBlockedOnce.remove(commandSpec.getGroupAddress())) {
getClient().writeToKNX(commandSpec);
if (isControl(channelUID)) {
rememberRespondingSpec(commandSpec, true);
}
} else {
logger.debug(
"None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
channelUID, command, command.getClass().getSimpleName());
if (CHANNEL_RESET.equals(channelUID.getId())) {
if (address != null) {
restart();
}
} else {
try {
OutboundSpec commandSpec = knxChannel.getCommandSpec(command);
// only send GroupValueWrite to KNX if GA is not blocked once
if (commandSpec != null) {
GroupAddress destination = commandSpec.getGroupAddress();
if (knxChannel.isControl()) {
// always remember, otherwise we might send an old state
groupAddressesRespondingSpec.put(destination, commandSpec);
}
});
break;
if (groupAddressesWriteBlocked.get(destination) != null) {
logger.debug("Write to {} blocked for 1s/one call after read.", destination);
groupAddressesWriteBlocked.invalidate(destination);
} else {
getClient().writeToKNX(commandSpec);
}
} else {
logger.debug(
"None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
channelUID, command, command.getClass().getSimpleName());
}
} catch (KNXException e) {
logger.warn("An error occurred while handling command '{}' on channel '{}': {}", command,
channelUID, e.getMessage());
}
}
}
}
private boolean isControl(ChannelUID channelUID) {
ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID);
return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId());
}
private ChannelTypeUID getChannelTypeUID(ChannelUID channelUID) {
Channel channel = getThing().getChannel(channelUID.getId());
Objects.requireNonNull(channel);
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
Objects.requireNonNull(channelTypeUID);
return channelTypeUID;
}
/** KNXIO */
private void sendGroupValueResponse(Channel channel, GroupAddress destination) {
Set<GroupAddress> rsa = getKNXChannelType(channel).getWriteAddresses(channel.getConfiguration());
private void sendGroupValueResponse(ChannelUID channelUID, GroupAddress destination) {
KNXChannel knxChannel = knxChannels.get(channelUID);
if (knxChannel == null) {
return;
}
Set<GroupAddress> rsa = knxChannel.getWriteAddresses();
if (!rsa.isEmpty()) {
logger.trace("onGroupRead size '{}'", rsa.size());
withKNXType(channel, (selector, configuration) -> {
Optional<OutboundSpec> os = groupAddressesRespondingSpec.stream().filter(spec -> {
GroupAddress groupAddress = spec.getGroupAddress();
if (groupAddress != null) {
return groupAddress.equals(destination);
}
return false;
}).findFirst();
if (os.isPresent()) {
logger.trace("onGroupRead respondToKNX '{}'", os.get().getGroupAddress());
/** KNXIO: sending real "GroupValueResponse" to the KNX bus. */
getClient().respondToKNX(os.get());
OutboundSpec os = groupAddressesRespondingSpec.get(destination);
if (os != null) {
logger.trace("onGroupRead respondToKNX '{}'",
os.getGroupAddress()); /* KNXIO: sending real "GroupValueResponse" to the KNX bus. */
try {
getClient().respondToKNX(os);
} catch (KNXException e) {
logger.warn("An error occurred on channel {}: {}", channelUID, e.getMessage(), e);
}
});
}
}
}
@ -308,22 +268,25 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu) {
logger.trace("onGroupRead Thing '{}' received a GroupValueRead telegram from '{}' for destination '{}'",
getThing().getUID(), source, destination);
for (Channel channel : getThing().getChannels()) {
if (isControl(channel.getUID())) {
withKNXType(channel, (selector, configuration) -> {
OutboundSpec responseSpec = selector.getResponseSpec(configuration, destination,
RefreshType.REFRESH);
if (responseSpec != null) {
logger.trace("onGroupRead isControl -> postCommand");
// This event should be sent to KNX as GroupValueResponse immediately.
sendGroupValueResponse(channel, destination);
// Send REFRESH to openHAB to get this event for scripting with postCommand
// and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after
// postCommand is done!
groupAddressesWriteBlockedOnce.add(destination);
postCommand(channel.getUID().getId(), RefreshType.REFRESH);
for (KNXChannel knxChannel : knxChannels.values()) {
if (knxChannel.isControl()) {
OutboundSpec responseSpec = knxChannel.getResponseSpec(destination, RefreshType.REFRESH);
if (responseSpec != null) {
logger.trace("onGroupRead isControl -> postCommand");
// This event should be sent to KNX as GroupValueResponse immediately.
sendGroupValueResponse(knxChannel.getChannelUID(), destination);
// block write attempts for 1s or 1 request to prevent loops
if (!groupAddressesWriteBlocked.containsKey(destination)) {
groupAddressesWriteBlocked.put(destination, () -> null);
}
});
groupAddressesWriteBlocked.putValue(destination, true);
// Send REFRESH to openHAB to get this event for scripting with postCommand
// and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after
// postCommand is done!
postCommand(knxChannel.getChannelUID(), RefreshType.REFRESH);
}
}
}
}
@ -346,91 +309,219 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'",
getThing().getUID(), source, destination);
for (Channel channel : getThing().getChannels()) {
withKNXType(channel, (selector, configuration) -> {
InboundSpec listenSpec = selector.getListenSpec(configuration, destination);
if (listenSpec != null) {
logger.trace(
"onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
getThing().getUID(), destination, channel.getUID());
/**
* Remember current KNXIO outboundSpec only if it is a control channel.
*/
if (isControl(channel.getUID())) {
logger.trace("onGroupWrite isControl");
Type type = typeHelper.toType(
new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT()),
asdu);
if (type != null) {
OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type);
if (commandSpec != null) {
rememberRespondingSpec(commandSpec, true);
}
for (KNXChannel knxChannel : knxChannels.values()) {
InboundSpec listenSpec = knxChannel.getListenSpec(destination);
if (listenSpec != null) {
logger.trace(
"onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
getThing().getUID(), destination, knxChannel.getChannelUID());
/**
* Remember current KNXIO outboundSpec only if it is a control channel.
*/
if (knxChannel.isControl()) {
logger.trace("onGroupWrite isControl");
Type value = ValueDecoder.decode(listenSpec.getDPT(), asdu, knxChannel.preferredType());
if (value != null) {
OutboundSpec commandSpec = knxChannel.getCommandSpec(value);
if (commandSpec != null) {
groupAddressesRespondingSpec.put(destination, commandSpec);
}
}
processDataReceived(destination, asdu, listenSpec, channel.getUID());
}
});
processDataReceived(destination, asdu, listenSpec, knxChannel);
}
}
}
private void processDataReceived(GroupAddress destination, byte[] asdu, InboundSpec listenSpec,
ChannelUID channelUID) {
if (!isDPTSupported(listenSpec.getDPT())) {
KNXChannel knxChannel) {
if (DPTUtil.getAllowedTypes(listenSpec.getDPT()).isEmpty()) {
logger.warn("DPT '{}' is not supported by the KNX binding.", listenSpec.getDPT());
return;
}
Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT());
Type type = typeHelper.toType(datapoint, asdu);
if (type != null) {
if (isControl(channelUID)) {
Channel channel = getThing().getChannel(channelUID.getId());
Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY)
: null;
int frequency = repeat != null ? ((BigDecimal) repeat).intValue() : 0;
if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(getChannelTypeUID(channelUID).getId())
&& (type instanceof UnDefType || type instanceof IncreaseDecreaseType) && frequency > 0) {
Type value = ValueDecoder.decode(listenSpec.getDPT(), asdu, knxChannel.preferredType());
if (value != null) {
if (knxChannel.isControl()) {
ChannelUID channelUID = knxChannel.getChannelUID();
int frequency;
if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(knxChannel.getChannelType())) {
// if we have a dimmer control channel, check if a frequency is defined
Channel channel = getThing().getChannel(channelUID);
if (channel == null) {
logger.warn("Failed to find channel for ChannelUID '{}'", channelUID);
return;
}
frequency = ((BigDecimal) Objects.requireNonNullElse(
channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY), BigDecimal.ZERO))
.intValue();
} else {
// disable dimming by binding
frequency = 0;
}
if ((value instanceof UnDefType || value instanceof IncreaseDecreaseType) && frequency > 0) {
// continuous dimming by the binding
if (UnDefType.UNDEF.equals(type)) {
channelFutures.computeIfPresent(channelUID, (k, v) -> {
v.cancel(false);
return null;
});
} else if (type instanceof IncreaseDecreaseType) {
channelFutures.compute(channelUID, (k, v) -> {
if (v != null) {
v.cancel(true);
}
return scheduler.scheduleWithFixedDelay(() -> postCommand(channelUID, (Command) type), 0,
frequency, TimeUnit.MILLISECONDS);
});
// cancel a running scheduler before adding a new (and only add if not UnDefType)
ScheduledFuture<?> oldFuture = channelFutures.remove(channelUID);
if (oldFuture != null) {
oldFuture.cancel(true);
}
if (value instanceof IncreaseDecreaseType) {
channelFutures.put(channelUID, scheduler.scheduleWithFixedDelay(
() -> postCommand(channelUID, (Command) value), 0, frequency, TimeUnit.MILLISECONDS));
}
} else {
if (type instanceof Command) {
if (value instanceof Command command) {
logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address);
postCommand(channelUID, (Command) type);
postCommand(channelUID, command);
}
}
} else {
if (type instanceof State && !(type instanceof UnDefType)) {
updateState(channelUID, (State) type);
if (value instanceof State state && !(value instanceof UnDefType)) {
updateState(knxChannel.getChannelUID(), state);
}
}
} else {
String s = asduToHex(asdu);
logger.warn(
"Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')",
destination, datapoint, s);
"Ignoring KNX bus data for channel '{}': couldn't transform to any Type (GA='{}', DPT='{}', data='{}')",
knxChannel.getChannelUID(), destination, listenSpec.getDPT(), HexUtils.bytesToHex(asdu));
}
}
private boolean isDPTSupported(@Nullable String dpt) {
return typeHelper.toTypeClass(dpt) != null;
protected final ScheduledExecutorService getScheduler() {
return getBridgeHandler().getScheduler();
}
private KNXChannelType getKNXChannelType(Channel channel) {
return KNXChannelTypes.getType(channel.getChannelTypeUID());
protected final ScheduledExecutorService getBackgroundScheduler() {
return getBridgeHandler().getBackgroundScheduler();
}
protected final KNXBridgeBaseThingHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
KNXBridgeBaseThingHandler handler = (KNXBridgeBaseThingHandler) bridge.getHandler();
if (handler != null) {
return handler;
}
}
throw new IllegalStateException("The bridge must not be null and must be initialized");
}
protected final KNXClient getClient() {
return getBridgeHandler().getClient();
}
protected final boolean describeDevice(@Nullable IndividualAddress address) {
if (address == null) {
return false;
}
DeviceInspector inspector = new DeviceInspector(getClient().getDeviceInfoClient(), address);
DeviceInspector.Result result = inspector.readDeviceInfo();
if (result != null) {
Map<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 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);
}
}
}
}

View File

@ -7,4 +7,12 @@
<name>KNX Binding</name>
<description>This binding supports connecting to a KNX bus</description>
<config-description>
<parameter name="disableUoM" type="boolean">
<default>false</default>
<label>Disable UoM</label>
<description>This disables Units of Measurement support for incoming values.</description>
</parameter>
</config-description>
</addon:addon>

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
*
* @author Holger Friedrich - Initial Contribution
*
*/
@NonNullByDefault
class KNXChannelFactoryTest {
/**
* This test checks if channels with invalid channelTypeUID lead to the intended exception.
* Side effect is testing if KNXChannelFactory can be instantiated (this is not the case e.g. when types with
* duplicate channel types are created)
*/
@Test
public void testNullChannelUidFails() {
Channel channel = mock(Channel.class);
assertThrows(IllegalArgumentException.class, () -> {
KNXChannelFactory.createKnxChannel(channel);
});
}
@Test
public void testInvalidChannelUidFails() {
Channel channel = mock(Channel.class);
when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("a:b:c"));
assertThrows(IllegalArgumentException.class, () -> {
KNXChannelFactory.createKnxChannel(channel);
});
}
@ParameterizedTest
@ValueSource(strings = { CHANNEL_COLOR, CHANNEL_COLOR_CONTROL, CHANNEL_CONTACT, CHANNEL_CONTACT_CONTROL,
CHANNEL_DATETIME, CHANNEL_DATETIME_CONTROL, CHANNEL_DIMMER, CHANNEL_DIMMER_CONTROL, CHANNEL_NUMBER,
CHANNEL_NUMBER_CONTROL, CHANNEL_ROLLERSHUTTER, CHANNEL_ROLLERSHUTTER_CONTROL, CHANNEL_STRING,
CHANNEL_STRING_CONTROL, CHANNEL_SWITCH, CHANNEL_SWITCH_CONTROL })
public void testSuccess(String channeltype) {
Channel channel = mock(Channel.class);
Configuration configuration = new Configuration(
Map.of("key1", "5.001:<1/2/3+4/5/6+1/5/6", "key2", "1.001:7/1/9+1/1/2"));
when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("knx:" + channeltype));
when(channel.getConfiguration()).thenReturn(configuration);
when(channel.getAcceptedItemType()).thenReturn("none");
assertNotNull(KNXChannelFactory.createKnxChannel(channel));
}
}

View File

@ -0,0 +1,170 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.UnDefType;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXFormatException;
/**
*
* @author Simon Kaufmann - Initial contribution
*
*/
@NonNullByDefault
class KNXChannelTest {
@Test
public void invalidFails() {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<1/3/22+0/3/22+<0/8/15");
assertNull(res);
}
@Test
void testParseWithDptMultipleWithRead() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<1/3/22+0/3/22+<0/7/15");
if (res == null) {
fail();
return;
}
assertEquals("5.001", res.getDPT());
assertEquals(new GroupAddress("1/3/22"), res.getMainGA());
assertTrue(res.getReadGAs().contains(res.getMainGA()));
assertEquals(3, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
@Test
void testParseWithDptMultipleWithoutRead() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:1/3/22+0/3/22+0/7/15");
if (res == null) {
fail();
return;
}
assertEquals("5.001", res.getDPT());
assertEquals(new GroupAddress("1/3/22"), res.getMainGA());
assertFalse(res.getReadGAs().contains(res.getMainGA()));
assertEquals(3, res.getListenGAs().size());
assertEquals(0, res.getReadGAs().size());
}
@Test
void testParseWithoutDptSingleWithoutRead() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("1/3/22");
if (res == null) {
fail();
return;
}
assertNull(res.getDPT());
assertEquals(new GroupAddress("1/3/22"), res.getMainGA());
assertFalse(res.getReadGAs().contains(res.getMainGA()));
assertEquals(1, res.getListenGAs().size());
assertEquals(0, res.getReadGAs().size());
}
@Test
void testParseWithoutDptSingleWithRead() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("<1/3/22");
if (res == null) {
fail();
return;
}
assertNull(res.getDPT());
assertEquals(new GroupAddress("1/3/22"), res.getMainGA());
assertTrue(res.getReadGAs().contains(res.getMainGA()));
assertEquals(1, res.getListenGAs().size());
assertEquals(1, res.getReadGAs().size());
}
@Test
void testParseTwoLevel() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<3/1024+<4/1025");
if (res == null) {
fail();
return;
}
assertEquals(new GroupAddress("3/1024"), res.getMainGA());
assertTrue(res.getReadGAs().contains(res.getMainGA()));
assertEquals(2, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
@Test
void testParseFreeLevel() throws KNXFormatException {
GroupAddressConfiguration res = GroupAddressConfiguration.parse("5.001:<4610+<4611");
if (res == null) {
fail();
return;
}
assertEquals(new GroupAddress("4610"), res.getMainGA());
assertEquals(2, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
@Test
public void testChannelGaParsing() throws KNXFormatException {
Channel channel = mock(Channel.class);
Configuration configuration = new Configuration(
Map.of("key1", "5.001:<1/2/3+4/5/6+1/5/6", "key2", "1.001:7/1/9+1/1/2"));
when(channel.getChannelTypeUID()).thenReturn(new ChannelTypeUID("a:b:c"));
when(channel.getConfiguration()).thenReturn(configuration);
when(channel.getAcceptedItemType()).thenReturn("none");
MyKNXChannel knxChannel = new MyKNXChannel(channel);
Set<GroupAddress> listenAddresses = knxChannel.getAllGroupAddresses();
assertEquals(5, listenAddresses.size());
// we don't check the content since parsing has been checked before and the quantity is correct
Set<GroupAddress> writeAddresses = knxChannel.getWriteAddresses();
assertEquals(2, writeAddresses.size());
assertTrue(writeAddresses.contains(new GroupAddress("1/2/3")));
assertTrue(writeAddresses.contains(new GroupAddress("7/1/9")));
}
private static class MyKNXChannel extends KNXChannel {
public MyKNXChannel(Channel channel) {
super(Set.of("key1", "key2"), List.of(UnDefType.class), channel);
}
@Override
protected String getDefaultDPT(String gaConfigKey) {
return "";
}
}
}

View File

@ -1,146 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.channel;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
*
* @author Simon Kaufmann - initial contribution and API.
*
*/
@NonNullByDefault
class KNXChannelTypeTest {
private KNXChannelType ct = new MyKNXChannelType("");
@BeforeEach
void setup() {
ct = new MyKNXChannelType("");
}
@Test
void testParseWithDptMultipleWithRead() {
ChannelConfiguration res = ct.parse("5.001:<1/3/22+0/3/22+<0/8/15");
if (res == null) {
fail();
return;
}
assertEquals("5.001", res.getDPT());
assertEquals("1/3/22", res.getMainGA().getGA());
assertTrue(res.getMainGA().isRead());
assertEquals(3, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
@Test
void testParseWithDptMultipleWithoutRead() {
ChannelConfiguration res = ct.parse("5.001:1/3/22+0/3/22+0/8/15");
if (res == null) {
fail();
return;
}
assertEquals("5.001", res.getDPT());
assertEquals("1/3/22", res.getMainGA().getGA());
assertFalse(res.getMainGA().isRead());
assertEquals(3, res.getListenGAs().size());
assertEquals(0, res.getReadGAs().size());
}
@Test
void testParseWithoutDptSingleWithoutRead() {
ChannelConfiguration res = ct.parse("1/3/22");
if (res == null) {
fail();
return;
}
assertNull(res.getDPT());
assertEquals("1/3/22", res.getMainGA().getGA());
assertFalse(res.getMainGA().isRead());
assertEquals(1, res.getListenGAs().size());
assertEquals(0, res.getReadGAs().size());
}
@Test
void testParseWithoutDptSingleWitRead() {
ChannelConfiguration res = ct.parse("<1/3/22");
if (res == null) {
fail();
return;
}
assertNull(res.getDPT());
assertEquals("1/3/22", res.getMainGA().getGA());
assertTrue(res.getMainGA().isRead());
assertEquals(1, res.getListenGAs().size());
assertEquals(1, res.getReadGAs().size());
}
@Test
void testParseTwoLevel() {
ChannelConfiguration res = ct.parse("5.001:<3/1024+<4/1025");
if (res == null) {
fail();
return;
}
assertEquals("3/1024", res.getMainGA().getGA());
assertEquals(2, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
@Test
void testParseFreeLevel() {
ChannelConfiguration res = ct.parse("5.001:<4610+<4611");
if (res == null) {
fail();
return;
}
assertEquals("4610", res.getMainGA().getGA());
assertEquals(2, res.getListenGAs().size());
assertEquals(2, res.getReadGAs().size());
}
private static class MyKNXChannelType extends KNXChannelType {
public MyKNXChannelType(String channelTypeID) {
super(channelTypeID);
}
@Override
protected Set<String> getAllGAKeys() {
return Collections.emptySet();
}
@Override
protected String getDefaultDPT(String gaConfigKey) {
return "";
}
}
}

View File

@ -0,0 +1,411 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import tuwien.auto.calimero.dptxlator.DPTXlator2ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteFloat;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator4ByteUnsigned;
import tuwien.auto.calimero.dptxlator.DPTXlator64BitSigned;
import tuwien.auto.calimero.dptxlator.DPTXlator8BitSigned;
import tuwien.auto.calimero.dptxlator.DptXlator2ByteSigned;
/**
*
* @author Simon Kaufmann - Initial contribution
*
*/
@NonNullByDefault
class DPTTest {
@Test
void testToDPTValueTrailingZeroesStrippedOff() {
assertEquals("3", ValueEncoder.encode(new DecimalType("3"), "17.001"));
assertEquals("3", ValueEncoder.encode(new DecimalType("3.0"), "17.001"));
}
@Test
public void testToDPTValueDecimalType() {
assertEquals("23.1", ValueEncoder.encode(new DecimalType("23.1"), "9.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT5ValueFromQuantityType() {
assertEquals("80", ValueEncoder.encode(new QuantityType<>("80 %"), "5.001"));
assertEquals("180", ValueEncoder.encode(new QuantityType<>("180 °"), "5.003"));
assertTrue(ValueEncoder.encode(new QuantityType<>("3.14 rad"), "5.003").startsWith("179."));
assertEquals("80", ValueEncoder.encode(new QuantityType<>("80 %"), "5.004"));
}
@Test
@SuppressWarnings("null")
void testToDPT7ValueFromQuantityType() {
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.002"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.003"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.004"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 ms"), "7.005"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 s"), "7.006"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 min"), "7.007"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 m"), "7.011"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 mA"), "7.012"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 lx"), "7.013"));
assertEquals("3000", ValueEncoder.encode(new QuantityType<>("3000 K"), "7.600"));
}
@Test
@SuppressWarnings("null")
void testToDPT8ValueFromQuantityType() {
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.002"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.003"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.004"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 ms"), "8.005"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 s"), "8.006"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("60 min"), "8.007"));
assertEquals("180", ValueEncoder.encode(new QuantityType<>("180 °"), "8.011"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 km"), "8.012"));
}
@Test
@SuppressWarnings("null")
void testToDPT9ValueFromQuantityType() {
assertEquals("23.1", ValueEncoder.encode(new QuantityType<>("23.1 °C"), "9.001"));
assertEquals(5.0,
Double.parseDouble(Objects.requireNonNull(ValueEncoder.encode(new QuantityType<>("41 °F"), "9.001"))));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("274.15 K"), "9.001"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "9.002"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 mK"), "9.002"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C"), "9.002"));
assertTrue(ValueEncoder.encode(new QuantityType<>("1 °F"), "9.002").startsWith("0.55"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K/h"), "9.003"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C/h"), "9.003"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1000 mK/h"), "9.003"));
assertEquals("600", ValueEncoder.encode(new QuantityType<>("10 K/min"), "9.003"));
assertEquals("100", ValueEncoder.encode(new QuantityType<>("100 lx"), "9.004"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s"), "9.005"));
assertTrue(ValueEncoder.encode(new QuantityType<>("1.94 kn"), "9.005").startsWith("0.99"));
assertEquals(1.0, Double
.parseDouble(Objects.requireNonNull(ValueEncoder.encode(new QuantityType<>("3.6 km/h"), "9.005"))));
assertEquals("456", ValueEncoder.encode(new QuantityType<>("456 Pa"), "9.006"));
assertEquals("70", ValueEncoder.encode(new QuantityType<>("70 %"), "9.007"));
assertEquals("8", ValueEncoder.encode(new QuantityType<>("8 ppm"), "9.008"));
assertEquals("9", ValueEncoder.encode(new QuantityType<>("9 m³/h"), "9.009"));
assertEquals("10", ValueEncoder.encode(new QuantityType<>("10 s"), "9.010"));
assertEquals("11", ValueEncoder.encode(new QuantityType<>("0.011 s"), "9.011"));
assertEquals("20", ValueEncoder.encode(new QuantityType<>("20 mV"), "9.020"));
assertEquals("20", ValueEncoder.encode(new QuantityType<>("0.02 V"), "9.020"));
assertEquals("21", ValueEncoder.encode(new QuantityType<>("21 mA"), "9.021"));
assertEquals("21", ValueEncoder.encode(new QuantityType<>("0.021 A"), "9.021"));
assertEquals("12", ValueEncoder.encode(new QuantityType<>("12 W/m²"), "9.022"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K/%"), "9.023"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C/%"), "9.023"));
assertTrue(ValueEncoder.encode(new QuantityType<>("1 °F/%"), "9.023").startsWith("0.55"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kW"), "9.024"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/h"), "9.025"));
assertEquals("60", ValueEncoder.encode(new QuantityType<>("1 l/min"), "9.025"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/m²"), "9.026"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °F"), "9.027"));
assertTrue(ValueEncoder.encode(new QuantityType<>("-12 °C"), "9.027").startsWith("10."));
assertEquals("10", ValueEncoder.encode(new QuantityType<>("10 km/h"), "9.028"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 g/m³"), "9.029"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 µg/m³"), "9.030"));
}
@Test
@SuppressWarnings("null")
void testToDPT10ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("Wed, 17:30:00", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "10.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT11ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("2019-06-12", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "11.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT12ValueFromQuantityType() {
// 12.001: dimensionless
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 s"), "12.100"));
assertEquals("2", ValueEncoder.encode(new QuantityType<>("2 min"), "12.101"));
assertEquals("3", ValueEncoder.encode(new QuantityType<>("3 h"), "12.102"));
assertEquals("1000", ValueEncoder.encode(new QuantityType<>("1 m^3"), "12.1200"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l"), "12.1200"));
assertEquals("2", ValueEncoder.encode(new QuantityType<>("2 m³"), "12.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT13ValueFromQuantityType() {
// 13.001 dimensionless
assertEquals("24", ValueEncoder.encode(new QuantityType<>("24 m³/h"), "13.002"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("24 m³/d"), "13.002"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 Wh"), "13.010"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 VAh"), "13.011"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 varh"), "13.012"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 kWh"), "13.013"));
assertEquals("4.2", ValueEncoder.encode(new QuantityType<>("4200 VAh"), "13.014"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 kvarh"), "13.015"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 MWh"), "13.016"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 s"), "13.100"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 l"), "13.1200"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 m³"), "13.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT14ValueFromQuantityType() {
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s²"), "14.000"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s²"), "14.001"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/mol"), "14.002"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 /s"), "14.003"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 mol"), "14.004"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad"), "14.006"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °"), "14.007"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J*s"), "14.008"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s"), "14.009"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m²"), "14.010"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 F"), "14.011"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.012"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m³"), "14.013"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m²/N"), "14.014"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 S"), "14.015"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 S/m"), "14.016"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg/m³"), "14.017"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C"), "14.018"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A"), "14.019"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m²"), "14.020"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C*m"), "14.021"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.022"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V/m"), "14.023"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C"), "14.024"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.025"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 C/m²"), "14.026"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.027"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.028"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A*m²"), "14.029"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V"), "14.030"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.031"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N"), "14.032"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Hz"), "14.033"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad/s"), "14.034"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/K"), "14.035"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W"), "14.036"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.037"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.038"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m"), "14.039"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.040"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 lm*s"), "14.040"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 cd/m²"), "14.041"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 lm"), "14.042"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 cd"), "14.043"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m"), "14.044"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Wb"), "14.045"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 T"), "14.046"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A*m²"), "14.047"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 T"), "14.048"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A/m"), "14.049"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 A"), "14.050"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg"), "14.051"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 kg/s"), "14.052"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N/s"), "14.053"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 rad"), "14.054"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °"), "14.055"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W"), "14.056"));
// 14.057: dimensionless
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Pa"), "14.058"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.059"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm"), "14.060"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Ohm*m"), "14.061"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 H"), "14.062"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 sr"), "14.063"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W/m²"), "14.064"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m/s"), "14.065"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 Pa"), "14.066"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N/m"), "14.067"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 °C"), "14.068"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "14.069"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 K"), "14.070"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J/K"), "14.071"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 W/m/K"), "14.072"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 V/K"), "14.073"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 s"), "14.074"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N*m"), "14.075"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.075"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³"), "14.076"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³/s"), "14.077"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 N"), "14.078"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 J"), "14.079"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 VA"), "14.080"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 m³/h"), "14.1200"));
assertEquals("1", ValueEncoder.encode(new QuantityType<>("1 l/s"), "14.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT19ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("2019-06-12 17:30:00", ValueEncoder.encode(new DateTimeType("2019-06-12T17:30:00Z"), "19.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT29ValueFromQuantityType() {
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 Wh"), "29.010"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 VAh"), "29.011"));
assertEquals("42", ValueEncoder.encode(new QuantityType<>("42 varh"), "29.012"));
}
@Test
public void dpt232RgbValue() {
// input data
byte[] data = new byte[] { 123, 45, 67 };
// this is the old implementation
String value = "r:123 g:45 b:67";
int r = Integer.parseInt(value.split(" ")[0].split(":")[1]);
int g = Integer.parseInt(value.split(" ")[1].split(":")[1]);
int b = Integer.parseInt(value.split(" ")[2].split(":")[1]);
HSBType expected = HSBType.fromRGB(r, g, b);
assertEquals(expected, ValueDecoder.decode("232.600", data, HSBType.class));
}
@Test
public void dpt232HsbValue() {
// input data
byte[] data = new byte[] { 123, 45, 67 };
HSBType hsbType = (HSBType) ValueDecoder.decode("232.60000", data, HSBType.class);
Assertions.assertNotNull(hsbType);
Objects.requireNonNull(hsbType);
assertEquals(173.6, hsbType.getHue().doubleValue(), 0.1);
assertEquals(17.6, hsbType.getSaturation().doubleValue(), 0.1);
assertEquals(26.3, hsbType.getBrightness().doubleValue(), 0.1);
}
@Test
public void dpt252EncoderTest() {
// input data
byte[] data = new byte[] { 0x26, 0x2b, 0x31, 0x00, 0x00, 0x0e };
HSBType hsbType = (HSBType) ValueDecoder.decode("251.600", data, HSBType.class);
assertNotNull(hsbType);
assertEquals(207, hsbType.getHue().doubleValue(), 0.1);
assertEquals(22, hsbType.getSaturation().doubleValue(), 0.1);
assertEquals(18, hsbType.getBrightness().doubleValue(), 0.1);
}
// This test checks all our overrides for units. It allows to detect unnecessary overrides when we
// update Calimero library
@Test
public void unitFixes() {
// 8bit signed (DPT 6)
assertEquals(DPTXlator8BitSigned.DPT_PERCENT_V8.getUnit(), Units.PERCENT.getSymbol());
// two byte unsigned (DPT 7)
assertNotEquals("", DPTXlator2ByteUnsigned.DPT_VALUE_2_UCOUNT.getUnit()); // counts have no unit
assertNotEquals(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_10.getUnit(), "ms"); // according to spec, it is ms
assertNotEquals(DPTXlator2ByteUnsigned.DPT_TIMEPERIOD_100.getUnit(), "ms"); // according to spec, it is ms
// two byte signed (DPT 8, DPTXlator is missing in calimero 2.5-M1)
assertNotEquals("", DptXlator2ByteSigned.DptValueCount.getUnit()); // pulses habe no unit
// 4 byte unsigned (DPT 12)
assertNotEquals("", DPTXlator4ByteUnsigned.DPT_VALUE_4_UCOUNT.getUnit()); // counts have no unit
// 4 byte signed (DPT 13)
assertNotEquals(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY.getUnit(), Units.VAR_HOUR.toString());
assertNotEquals(DPTXlator4ByteSigned.DPT_REACTIVE_ENERGY_KVARH.getUnit(), Units.KILOVAR_HOUR.toString());
assertNotEquals(DPTXlator4ByteSigned.DPT_APPARENT_ENERGY_KVAH.getUnit(), "kVA*h");
assertNotEquals(DPTXlator4ByteSigned.DPT_FLOWRATE.getUnit(), Units.CUBICMETRE_PER_HOUR.toString());
assertNotEquals("", DPTXlator4ByteSigned.DPT_COUNT.getUnit()); // counts have no unit
// four byte float (DPT 14)
assertNotEquals(DPTXlator4ByteFloat.DPT_CONDUCTANCE.getUnit(), Units.SIEMENS.toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ANGULAR_MOMENTUM.getUnit(),
Units.JOULE.multiply(Units.SECOND).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ACTIVITY.getUnit(), Units.BECQUEREL.toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRICAL_CONDUCTIVITY.getUnit(),
Units.SIEMENS.divide(SIUnits.METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_TORQUE.getUnit(), Units.NEWTON.multiply(SIUnits.METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_RESISTIVITY.getUnit(), Units.OHM.multiply(SIUnits.METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRIC_DIPOLEMOMENT.getUnit(),
Units.COULOMB.multiply(SIUnits.METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTRIC_FLUX.getUnit(), Units.VOLT.multiply(SIUnits.METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_MAGNETIC_MOMENT.getUnit(),
Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString());
assertNotEquals(DPTXlator4ByteFloat.DPT_ELECTROMAGNETIC_MOMENT.getUnit(),
Units.AMPERE.multiply(SIUnits.SQUARE_METRE).toString());
// 64 bit signed (DPT 29)
assertNotEquals(DPTXlator64BitSigned.DPT_REACTIVE_ENERGY.getUnit(), Units.VAR_HOUR.toString());
}
private static Stream<String> unitProvider() {
return DPTUnits.getAllUnitStrings();
}
@ParameterizedTest
@MethodSource("unitProvider")
public void unitsValid(String unit) {
String valueStr = "1 " + unit;
QuantityType<?> value = new QuantityType<>(valueStr);
Assertions.assertNotNull(value);
}
private static Stream<String> rgbValueProvider() {
return Stream.of("r:0 g:0 b:0", "r:255 g:255 b:255");
}
@ParameterizedTest
@MethodSource("rgbValueProvider")
public void rgbTest(String value) {
Assertions.assertNotNull(ValueDecoder.decode("232.600", value.getBytes(StandardCharsets.UTF_8), HSBType.class));
}
}

View File

@ -1,279 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.dpt;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
/**
*
* @author Simon Kaufmann - initial contribution and API
*
*/
@NonNullByDefault
class KNXCoreTypeMapperTest {
@Test
void testToDPTValueTrailingZeroesStrippedOff() {
assertEquals("3", new KNXCoreTypeMapper().toDPTValue(new DecimalType("3"), "17.001"));
assertEquals("3", new KNXCoreTypeMapper().toDPTValue(new DecimalType("3.0"), "17.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT5ValueFromQuantityType() {
assertEquals("80.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("80 %"), "5.001"));
assertEquals("180.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("180 °"), "5.003"));
assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3.14 rad"), "5.003").startsWith("179."));
assertEquals("80.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("80 %"), "5.004"));
}
@Test
@SuppressWarnings("null")
void testToDPT7ValueFromQuantityType() {
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.002"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.003"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.004"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "7.005"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 s"), "7.006"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 min"), "7.007"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m"), "7.011"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mA"), "7.012"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 lx"), "7.013"));
assertEquals("3000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3000 K"), "7.600"));
}
@Test
@SuppressWarnings("null")
void testToDPT8ValueFromQuantityType() {
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.002"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.003"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.004"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 ms"), "8.005"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 s"), "8.006"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("60 min"), "8.007"));
assertEquals("180.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("180 °"), "8.011"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 km"), "8.012"));
}
@Test
@SuppressWarnings("null")
void testToDPT9ValueFromQuantityType() {
assertEquals("23.1", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("23.1 °C"), "9.001"));
assertEquals("5.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("41 °F"), "9.001"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("274.15 K"), "9.001"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "9.002"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mK"), "9.002"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C"), "9.002"));
assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F"), "9.002").startsWith("0.55"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K/h"), "9.003"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C/h"), "9.003"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1000 mK/h"), "9.003"));
assertEquals("600.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 K/min"), "9.003"));
assertEquals("100.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("100 lx"), "9.004"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s"), "9.005"));
assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1.94 kn"), "9.005").startsWith("0.99"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3.6 km/h"), "9.005"));
assertEquals("456.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("456 Pa"), "9.006"));
assertEquals("70.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("70 %"), "9.007"));
assertEquals("8.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("8 ppm"), "9.008"));
assertEquals("9.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("9 m³/h"), "9.009"));
assertEquals("10.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 s"), "9.010"));
assertEquals("11.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.011 s"), "9.011"));
assertEquals("20.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("20 mV"), "9.020"));
assertEquals("20.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.02 V"), "9.020"));
assertEquals("21.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("21 mA"), "9.021"));
assertEquals("21.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("0.021 A"), "9.021"));
assertEquals("12.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("12 W/m²"), "9.022"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K/%"), "9.023"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C/%"), "9.023"));
assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F/%"), "9.023").startsWith("0.55"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kW"), "9.024"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/h"), "9.025"));
assertEquals("60.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/min"), "9.025"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/m²"), "9.026"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °F"), "9.027"));
assertTrue(new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("-12 °C"), "9.027").startsWith("10."));
assertEquals("10.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("10 km/h"), "9.028"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 g/m³"), "9.029"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 µg/m³"), "9.030"));
}
@Test
@SuppressWarnings("null")
void testToDPT10ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("Wed, 17:30:00",
new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "10.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT11ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("2019-06-12",
new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "11.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT12ValueFromQuantityType() {
// 12.001: dimensionless
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 s"), "12.100"));
assertEquals("2.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("2 min"), "12.101"));
assertEquals("3.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("3 h"), "12.102"));
assertEquals("1000.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m^3"), "12.1200"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l"), "12.1200"));
assertEquals("2.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("2 m³"), "12.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT13ValueFromQuantityType() {
// 13.001 dimensionless
assertEquals("24.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("24 m³/h"), "13.002"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("24 m³/d"), "13.002"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 Wh"), "13.010"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 VAh"), "13.011"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 varh"), "13.012"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 kWh"), "13.013"));
assertEquals("4.2", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("4200 VAh"), "13.014"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 kvarh"), "13.015"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 MWh"), "13.016"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 s"), "13.100"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 l"), "13.1200"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 m³"), "13.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT14ValueFromQuantityType() {
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s²"), "14.000"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s²"), "14.001"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/mol"), "14.002"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 /s"), "14.003"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 mol"), "14.004"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad"), "14.006"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °"), "14.007"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J*s"), "14.008"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s"), "14.009"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m²"), "14.010"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 F"), "14.011"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.012"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m³"), "14.013"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m²/N"), "14.014"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 S"), "14.015"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 S/m"), "14.016"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg/m³"), "14.017"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C"), "14.018"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A"), "14.019"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m²"), "14.020"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C*m"), "14.021"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.022"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V/m"), "14.023"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C"), "14.024"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.025"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 C/m²"), "14.026"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.027"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.028"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A*m²"), "14.029"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V"), "14.030"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.031"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N"), "14.032"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Hz"), "14.033"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad/s"), "14.034"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/K"), "14.035"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W"), "14.036"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.037"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.038"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m"), "14.039"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.040"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 lm*s"), "14.040"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 cd/m²"), "14.041"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 lm"), "14.042"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 cd"), "14.043"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m"), "14.044"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Wb"), "14.045"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 T"), "14.046"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A*m²"), "14.047"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 T"), "14.048"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A/m"), "14.049"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 A"), "14.050"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg"), "14.051"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 kg/s"), "14.052"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N/s"), "14.053"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 rad"), "14.054"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °"), "14.055"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W"), "14.056"));
// 14.057: dimensionless
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Pa"), "14.058"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.059"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm"), "14.060"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Ohm*m"), "14.061"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 H"), "14.062"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 sr"), "14.063"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W/m²"), "14.064"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m/s"), "14.065"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 Pa"), "14.066"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N/m"), "14.067"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 °C"), "14.068"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "14.069"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 K"), "14.070"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J/K"), "14.071"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 W/m/K"), "14.072"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 V/K"), "14.073"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 s"), "14.074"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N*m"), "14.075"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.075"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³"), "14.076"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³/s"), "14.077"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 N"), "14.078"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 J"), "14.079"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 VA"), "14.080"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 m³/h"), "14.1200"));
assertEquals("1.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("1 l/s"), "14.1201"));
}
@Test
@SuppressWarnings("null")
void testToDPT19ValueFromQuantityType() {
// DateTimeTyype, not QuantityType
assertEquals("2019-06-12 17:30:00",
new KNXCoreTypeMapper().toDPTValue(new DateTimeType("2019-06-12T17:30:00Z"), "19.001"));
}
@Test
@SuppressWarnings("null")
void testToDPT29ValueFromQuantityType() {
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 Wh"), "29.010"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 VAh"), "29.011"));
assertEquals("42.0", new KNXCoreTypeMapper().toDPTValue(new QuantityType<>("42 varh"), "29.012"));
}
}