2 * Copyright (c) 2010-2023 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.handler;
15 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.List;
20 import java.util.Objects;
21 import java.util.Optional;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.knx.internal.KNXBindingConstants;
30 import org.openhab.binding.knx.internal.KNXTypeMapper;
31 import org.openhab.binding.knx.internal.channel.KNXChannelType;
32 import org.openhab.binding.knx.internal.channel.KNXChannelTypes;
33 import org.openhab.binding.knx.internal.client.AbstractKNXClient;
34 import org.openhab.binding.knx.internal.client.InboundSpec;
35 import org.openhab.binding.knx.internal.client.OutboundSpec;
36 import org.openhab.binding.knx.internal.config.DeviceConfig;
37 import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
38 import org.openhab.core.config.core.Configuration;
39 import org.openhab.core.library.types.IncreaseDecreaseType;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.type.ChannelTypeUID;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.Type;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import tuwien.auto.calimero.GroupAddress;
53 import tuwien.auto.calimero.IndividualAddress;
54 import tuwien.auto.calimero.KNXException;
55 import tuwien.auto.calimero.KNXFormatException;
56 import tuwien.auto.calimero.datapoint.CommandDP;
57 import tuwien.auto.calimero.datapoint.Datapoint;
60 * The {@link DeviceThingHandler} is responsible for handling commands and state updates sent to and received from the
61 * bus and updating the channels correspondingly.
63 * @author Simon Kaufmann - Initial contribution and API
66 public class DeviceThingHandler extends AbstractKNXThingHandler {
68 private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class);
70 private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper();
71 private final Set<GroupAddress> groupAddresses = ConcurrentHashMap.newKeySet();
72 private final Set<GroupAddress> groupAddressesWriteBlockedOnce = ConcurrentHashMap.newKeySet();
73 private final Set<OutboundSpec> groupAddressesRespondingSpec = ConcurrentHashMap.newKeySet();
74 private final Map<GroupAddress, ScheduledFuture<?>> readFutures = new ConcurrentHashMap<>();
75 private final Map<ChannelUID, ScheduledFuture<?>> channelFutures = new ConcurrentHashMap<>();
76 private int readInterval;
78 public DeviceThingHandler(Thing thing) {
83 public void initialize() {
85 DeviceConfig config = getConfigAs(DeviceConfig.class);
86 readInterval = config.getReadInterval();
87 initializeGroupAddresses();
90 private void initializeGroupAddresses() {
91 forAllChannels((selector, channelConfiguration) -> {
92 groupAddresses.addAll(selector.getReadAddresses(channelConfiguration));
93 groupAddresses.addAll(selector.getWriteAddresses(channelConfiguration));
94 groupAddresses.addAll(selector.getListenAddresses(channelConfiguration));
99 public void dispose() {
100 cancelChannelFutures();
101 freeGroupAddresses();
105 private void cancelChannelFutures() {
106 for (ChannelUID channelUID : channelFutures.keySet()) {
107 channelFutures.computeIfPresent(channelUID, (k, v) -> {
114 private void freeGroupAddresses() {
115 groupAddresses.clear();
116 groupAddressesWriteBlockedOnce.clear();
117 groupAddressesRespondingSpec.clear();
121 protected void cancelReadFutures() {
122 for (GroupAddress groupAddress : readFutures.keySet()) {
123 readFutures.computeIfPresent(groupAddress, (k, v) -> {
131 private interface ChannelFunction {
132 void apply(KNXChannelType channelType, Configuration configuration) throws KNXException;
135 private void withKNXType(ChannelUID channelUID, ChannelFunction function) {
136 Channel channel = getThing().getChannel(channelUID.getId());
137 if (channel == null) {
138 logger.warn("Channel '{}' does not exist", channelUID);
141 withKNXType(channel, function);
144 private void withKNXType(Channel channel, ChannelFunction function) {
146 KNXChannelType selector = getKNXChannelType(channel);
147 function.apply(selector, channel.getConfiguration());
148 } catch (KNXException e) {
149 logger.warn("An error occurred on channel {}: {}", channel.getUID(), e.getMessage(), e);
153 private void forAllChannels(ChannelFunction function) {
154 for (Channel channel : getThing().getChannels()) {
155 withKNXType(channel, function);
160 public void channelLinked(ChannelUID channelUID) {
161 if (!isControl(channelUID)) {
162 withKNXType(channelUID, (selector, configuration) -> {
163 scheduleRead(selector, configuration);
169 protected void scheduleReadJobs() {
171 for (Channel channel : getThing().getChannels()) {
172 if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) {
173 withKNXType(channel, (selector, configuration) -> {
174 scheduleRead(selector, configuration);
180 private void scheduleRead(KNXChannelType selector, Configuration configuration) throws KNXFormatException {
181 List<InboundSpec> readSpecs = selector.getReadSpec(configuration);
182 for (InboundSpec readSpec : readSpecs) {
183 for (GroupAddress groupAddress : readSpec.getGroupAddresses()) {
184 scheduleReadJob(groupAddress, readSpec.getDPT());
189 private void scheduleReadJob(GroupAddress groupAddress, String dpt) {
190 if (readInterval > 0) {
191 ScheduledFuture<?> future = readFutures.get(groupAddress);
192 if (future == null || future.isDone() || future.isCancelled()) {
193 future = getScheduler().scheduleWithFixedDelay(() -> readDatapoint(groupAddress, dpt), 0, readInterval,
195 readFutures.put(groupAddress, future);
198 getScheduler().submit(() -> readDatapoint(groupAddress, dpt));
202 private void readDatapoint(GroupAddress groupAddress, String dpt) {
203 if (getClient().isConnected()) {
204 if (!isDPTSupported(dpt)) {
205 logger.warn("DPT '{}' is not supported by the KNX binding", dpt);
208 Datapoint datapoint = new CommandDP(groupAddress, getThing().getUID().toString(), 0, dpt);
209 getClient().readDatapoint(datapoint);
214 public boolean listensTo(GroupAddress destination) {
215 return groupAddresses.contains(destination);
218 /** KNXIO remember controls, removeIf may be null */
219 @SuppressWarnings("null")
220 private void rememberRespondingSpec(OutboundSpec commandSpec, boolean add) {
221 GroupAddress ga = commandSpec.getGroupAddress();
223 groupAddressesRespondingSpec.removeIf(spec -> spec.getGroupAddress().equals(ga));
226 groupAddressesRespondingSpec.add(commandSpec);
228 logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga,
229 groupAddressesRespondingSpec.size(), add);
232 /** Handling commands triggered from openHAB */
234 public void handleCommand(ChannelUID channelUID, Command command) {
235 logger.trace("Handling command '{}' for channel '{}'", command, channelUID);
236 if (command instanceof RefreshType && !isControl(channelUID)) {
237 logger.debug("Refreshing channel '{}'", channelUID);
238 withKNXType(channelUID, (selector, configuration) -> {
239 scheduleRead(selector, configuration);
242 switch (channelUID.getId()) {
244 if (address != null) {
249 withKNXType(channelUID, (selector, channelConfiguration) -> {
250 OutboundSpec commandSpec = selector.getCommandSpec(channelConfiguration, typeHelper, command);
251 // only send GroupValueWrite to KNX if GA is not blocked once
252 if (commandSpec != null
253 && !groupAddressesWriteBlockedOnce.remove(commandSpec.getGroupAddress())) {
254 getClient().writeToKNX(commandSpec);
255 if (isControl(channelUID)) {
256 rememberRespondingSpec(commandSpec, true);
260 "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
261 channelUID, command, command.getClass().getSimpleName());
269 private boolean isControl(ChannelUID channelUID) {
270 ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID);
271 return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId());
274 private ChannelTypeUID getChannelTypeUID(ChannelUID channelUID) {
275 Channel channel = getThing().getChannel(channelUID.getId());
276 Objects.requireNonNull(channel);
277 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
278 Objects.requireNonNull(channelTypeUID);
279 return channelTypeUID;
283 private void sendGroupValueResponse(Channel channel, GroupAddress destination) {
284 Set<GroupAddress> rsa = getKNXChannelType(channel).getWriteAddresses(channel.getConfiguration());
285 if (!rsa.isEmpty()) {
286 logger.trace("onGroupRead size '{}'", rsa.size());
287 withKNXType(channel, (selector, configuration) -> {
288 Optional<OutboundSpec> os = groupAddressesRespondingSpec.stream().filter(spec -> {
289 GroupAddress groupAddress = spec.getGroupAddress();
290 if (groupAddress != null) {
291 return groupAddress.equals(destination);
295 if (os.isPresent()) {
296 logger.trace("onGroupRead respondToKNX '{}'", os.get().getGroupAddress());
297 /** KNXIO: sending real "GroupValueResponse" to the KNX bus. */
298 getClient().respondToKNX(os.get());
305 * KNXIO, extended with the ability to respond on "GroupValueRead" telegrams with "GroupValueResponse" telegram
308 public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu) {
309 logger.trace("onGroupRead Thing '{}' received a GroupValueRead telegram from '{}' for destination '{}'",
310 getThing().getUID(), source, destination);
311 for (Channel channel : getThing().getChannels()) {
312 if (isControl(channel.getUID())) {
313 withKNXType(channel, (selector, configuration) -> {
314 OutboundSpec responseSpec = selector.getResponseSpec(configuration, destination,
315 RefreshType.REFRESH);
316 if (responseSpec != null) {
317 logger.trace("onGroupRead isControl -> postCommand");
318 // This event should be sent to KNX as GroupValueResponse immediately.
319 sendGroupValueResponse(channel, destination);
320 // Send REFRESH to openHAB to get this event for scripting with postCommand
321 // and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after
322 // postCommand is done!
323 groupAddressesWriteBlockedOnce.add(destination);
324 postCommand(channel.getUID().getId(), RefreshType.REFRESH);
332 public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
334 // GroupValueResponses are treated the same as GroupValueWrite telegrams
335 logger.trace("onGroupReadResponse Thing '{}' processes a GroupValueResponse telegram for destination '{}'",
336 getThing().getUID(), destination);
337 onGroupWrite(client, source, destination, asdu);
341 * KNXIO, here value changes are set, coming from KNX OR openHAB.
344 public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
346 logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'",
347 getThing().getUID(), source, destination);
349 for (Channel channel : getThing().getChannels()) {
350 withKNXType(channel, (selector, configuration) -> {
351 InboundSpec listenSpec = selector.getListenSpec(configuration, destination);
352 if (listenSpec != null) {
354 "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
355 getThing().getUID(), destination, channel.getUID());
357 * Remember current KNXIO outboundSpec only if it is a control channel.
359 if (isControl(channel.getUID())) {
360 logger.trace("onGroupWrite isControl");
361 Type type = typeHelper.toType(
362 new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT()),
365 OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type);
366 if (commandSpec != null) {
367 rememberRespondingSpec(commandSpec, true);
371 processDataReceived(destination, asdu, listenSpec, channel.getUID());
377 private void processDataReceived(GroupAddress destination, byte[] asdu, InboundSpec listenSpec,
378 ChannelUID channelUID) {
379 if (!isDPTSupported(listenSpec.getDPT())) {
380 logger.warn("DPT '{}' is not supported by the KNX binding.", listenSpec.getDPT());
384 Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT());
385 Type type = typeHelper.toType(datapoint, asdu);
388 if (isControl(channelUID)) {
389 Channel channel = getThing().getChannel(channelUID.getId());
390 Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY)
392 int frequency = repeat != null ? ((BigDecimal) repeat).intValue() : 0;
393 if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(getChannelTypeUID(channelUID).getId())
394 && (type instanceof UnDefType || type instanceof IncreaseDecreaseType) && frequency > 0) {
395 // continuous dimming by the binding
396 if (UnDefType.UNDEF.equals(type)) {
397 channelFutures.computeIfPresent(channelUID, (k, v) -> {
401 } else if (type instanceof IncreaseDecreaseType) {
402 channelFutures.compute(channelUID, (k, v) -> {
406 return scheduler.scheduleWithFixedDelay(() -> postCommand(channelUID, (Command) type), 0,
407 frequency, TimeUnit.MILLISECONDS);
411 if (type instanceof Command) {
412 logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address);
413 postCommand(channelUID, (Command) type);
417 if (type instanceof State && !(type instanceof UnDefType)) {
418 updateState(channelUID, (State) type);
422 String s = asduToHex(asdu);
424 "Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')",
425 destination, datapoint, s);
429 private boolean isDPTSupported(@Nullable String dpt) {
430 return typeHelper.toTypeClass(dpt) != null;
433 private KNXChannelType getKNXChannelType(Channel channel) {
434 return KNXChannelTypes.getType(channel.getChannelTypeUID());