2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.knx.internal.channel;
15 import static java.util.stream.Collectors.toList;
16 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
18 import java.util.ArrayList;
19 import java.util.LinkedHashMap;
20 import java.util.List;
22 import java.util.Objects;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.knx.internal.client.InboundSpec;
28 import org.openhab.binding.knx.internal.client.OutboundSpec;
29 import org.openhab.binding.knx.internal.dpt.DPTUtil;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.types.State;
34 import org.openhab.core.types.Type;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
38 import tuwien.auto.calimero.GroupAddress;
41 * Meta-data abstraction for the KNX channel configurations.
43 * @author Simon Kaufmann - initial contribution and API
44 * @author Jan N. Klug - refactored from type definition to channel instance
48 public abstract class KNXChannel {
49 private final Logger logger = LoggerFactory.getLogger(KNXChannel.class);
50 private final List<String> gaKeys;
52 private final Map<String, GroupAddressConfiguration> groupAddressConfigurations = new LinkedHashMap<>();
53 private final List<GroupAddress> listenAddresses = new ArrayList<>();
54 private final List<GroupAddress> writeAddresses = new ArrayList<>();
55 private final String channelType;
56 private final ChannelUID channelUID;
57 private final boolean isControl;
58 private final Class<? extends Type> preferredType;
60 KNXChannel(List<Class<? extends Type>> acceptedTypes, Channel channel) {
61 this(List.of(GA), acceptedTypes, channel);
64 KNXChannel(List<String> gaKeys, List<Class<? extends Type>> acceptedTypes, Channel channel) {
66 this.preferredType = acceptedTypes.get(0);
68 // this is safe because we already checked the presence of the ChannelTypeUID before
69 this.channelType = Objects.requireNonNull(channel.getChannelTypeUID()).getId();
70 this.channelUID = channel.getUID();
71 this.isControl = CONTROL_CHANNEL_TYPES.contains(channelType);
73 // build map of ChannelConfigurations and GA lists
74 Configuration configuration = channel.getConfiguration();
75 gaKeys.forEach(key -> {
76 GroupAddressConfiguration groupAddressConfiguration = GroupAddressConfiguration
77 .parse(configuration.get(key));
78 if (groupAddressConfiguration != null) {
79 // check DPT configuration (if set) is compatible with item
80 String dpt = groupAddressConfiguration.getDPT();
82 Set<Class<? extends Type>> types = DPTUtil.getAllowedTypes(dpt);
83 if (acceptedTypes.stream().noneMatch(types::contains)) {
84 logger.warn("Configured DPT '{}' is incompatible with accepted types '{}' for channel '{}'",
85 dpt, acceptedTypes, channelUID);
88 groupAddressConfigurations.put(key, groupAddressConfiguration);
89 // store address configuration for re-use
90 listenAddresses.addAll(groupAddressConfiguration.getListenGAs());
91 writeAddresses.add(groupAddressConfiguration.getMainGA());
96 public String getChannelType() {
100 public ChannelUID getChannelUID() {
104 public boolean isControl() {
108 public Class<? extends Type> preferredType() {
109 return preferredType;
112 public final List<GroupAddress> getAllGroupAddresses() {
113 return listenAddresses;
116 public final List<GroupAddress> getWriteAddresses() {
117 return writeAddresses;
120 public final @Nullable OutboundSpec getCommandSpec(Type command) {
121 logger.trace("getCommandSpec checking keys '{}' for command '{}' ({})", gaKeys, command, command.getClass());
122 // first check if there is a direct match for the provided command for all GAs
123 for (Map.Entry<String, GroupAddressConfiguration> entry : groupAddressConfigurations.entrySet()) {
124 String dpt = Objects.requireNonNullElse(entry.getValue().getDPT(), getDefaultDPT(entry.getKey()));
125 Set<Class<? extends Type>> expectedTypeClasses = DPTUtil.getAllowedTypes(dpt);
126 // find the first matching type that is assignable from the command
127 if (expectedTypeClasses.contains(command.getClass())) {
129 "getCommandSpec key '{}' has one of the expectedTypeClasses '{}', matching command '{}' and dpt '{}'",
130 entry.getKey(), expectedTypeClasses, command, dpt);
131 return new WriteSpecImpl(entry.getValue(), dpt, command);
134 // if we didn't find a match, check if we find a sub-type match
135 for (Map.Entry<String, GroupAddressConfiguration> entry : groupAddressConfigurations.entrySet()) {
136 String dpt = Objects.requireNonNullElse(entry.getValue().getDPT(), getDefaultDPT(entry.getKey()));
137 Set<Class<? extends Type>> expectedTypeClasses = DPTUtil.getAllowedTypes(dpt);
138 for (Class<? extends Type> expectedTypeClass : expectedTypeClasses) {
139 if (command instanceof State state && State.class.isAssignableFrom(expectedTypeClass)) {
140 var subClass = expectedTypeClass.asSubclass(State.class);
141 if (state.as(subClass) != null) {
143 "getCommandSpec command class '{}' is a sub-class of the expectedTypeClass '{}' for key '{}'",
144 command.getClass(), expectedTypeClass, entry.getKey());
145 Class<? extends State> expectedTypeAsStateClass = expectedTypeClass.asSubclass(State.class);
146 State convertedState = state.as(expectedTypeAsStateClass);
147 if (convertedState != null) {
148 return new WriteSpecImpl(entry.getValue(), dpt, convertedState);
155 "getCommandSpec could not match command class '{}' with expectedTypeClasses for any of the checked keys '{}', discarding command",
156 command.getClass(), gaKeys);
160 public final List<InboundSpec> getReadSpec() {
161 return groupAddressConfigurations.entrySet().stream()
162 .map(entry -> new ReadRequestSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey())))
163 .filter(spec -> !spec.getGroupAddresses().isEmpty()).collect(toList());
166 public final @Nullable InboundSpec getListenSpec(GroupAddress groupAddress) {
167 return groupAddressConfigurations.entrySet().stream()
168 .map(entry -> new ListenSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey())))
169 .filter(spec -> spec.getGroupAddresses().contains(groupAddress)).findFirst().orElse(null);
172 public final @Nullable OutboundSpec getResponseSpec(GroupAddress groupAddress, Type value) {
173 return groupAddressConfigurations.entrySet().stream()
174 .map(entry -> new ReadResponseSpecImpl(entry.getValue(), getDefaultDPT(entry.getKey()), value))
175 .filter(spec -> spec.matchesDestination(groupAddress)).findFirst().orElse(null);
178 protected abstract String getDefaultDPT(String gaConfigKey);