]> git.basschouten.com Git - openhab-addons.git/blob
b08a309203c3913037e5829e2d7463b2b0988639
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.knx.internal.channel;
14
15 import static java.util.stream.Collectors.toList;
16 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
17
18 import java.util.ArrayList;
19 import java.util.LinkedHashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Set;
24
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;
37
38 import tuwien.auto.calimero.GroupAddress;
39
40 /**
41  * Meta-data abstraction for the KNX channel configurations.
42  *
43  * @author Simon Kaufmann - initial contribution and API
44  * @author Jan N. Klug - refactored from type definition to channel instance
45  *
46  */
47 @NonNullByDefault
48 public abstract class KNXChannel {
49     private final Logger logger = LoggerFactory.getLogger(KNXChannel.class);
50     private final List<String> gaKeys;
51
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;
59
60     KNXChannel(List<Class<? extends Type>> acceptedTypes, Channel channel) {
61         this(List.of(GA), acceptedTypes, channel);
62     }
63
64     KNXChannel(List<String> gaKeys, List<Class<? extends Type>> acceptedTypes, Channel channel) {
65         this.gaKeys = gaKeys;
66         this.preferredType = acceptedTypes.get(0);
67
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);
72
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();
81                 if (dpt != null) {
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);
86                     }
87                 }
88                 groupAddressConfigurations.put(key, groupAddressConfiguration);
89                 // store address configuration for re-use
90                 listenAddresses.addAll(groupAddressConfiguration.getListenGAs());
91                 writeAddresses.add(groupAddressConfiguration.getMainGA());
92             }
93         });
94     }
95
96     public String getChannelType() {
97         return channelType;
98     }
99
100     public ChannelUID getChannelUID() {
101         return channelUID;
102     }
103
104     public boolean isControl() {
105         return isControl;
106     }
107
108     public Class<? extends Type> preferredType() {
109         return preferredType;
110     }
111
112     public final List<GroupAddress> getAllGroupAddresses() {
113         return listenAddresses;
114     }
115
116     public final List<GroupAddress> getWriteAddresses() {
117         return writeAddresses;
118     }
119
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())) {
128                 logger.trace(
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);
132             }
133         }
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) {
142                         logger.trace(
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);
149                         }
150                     }
151                 }
152             }
153         }
154         logger.trace(
155                 "getCommandSpec could not match command class '{}' with expectedTypeClasses for any of the checked keys '{}', discarding command",
156                 command.getClass(), gaKeys);
157         return null;
158     }
159
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());
164     }
165
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);
170     }
171
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);
176     }
177
178     protected abstract String getDefaultDPT(String gaConfigKey);
179 }