]> git.basschouten.com Git - openhab-addons.git/blob
aee86534ab45ac5ac1af239f89fc9fd5b6de0691
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.handler;
14
15 import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
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;
51
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;
58
59 /**
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.
62  *
63  * @author Simon Kaufmann - Initial contribution and API
64  */
65 @NonNullByDefault
66 public class DeviceThingHandler extends AbstractKNXThingHandler {
67
68     private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class);
69
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;
77
78     public DeviceThingHandler(Thing thing) {
79         super(thing);
80     }
81
82     @Override
83     public void initialize() {
84         super.initialize();
85         DeviceConfig config = getConfigAs(DeviceConfig.class);
86         readInterval = config.getReadInterval();
87         initializeGroupAddresses();
88     }
89
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));
95         });
96     }
97
98     @Override
99     public void dispose() {
100         cancelChannelFutures();
101         freeGroupAddresses();
102         super.dispose();
103     }
104
105     private void cancelChannelFutures() {
106         for (ChannelUID channelUID : channelFutures.keySet()) {
107             channelFutures.computeIfPresent(channelUID, (k, v) -> {
108                 v.cancel(true);
109                 return null;
110             });
111         }
112     }
113
114     private void freeGroupAddresses() {
115         groupAddresses.clear();
116         groupAddressesWriteBlockedOnce.clear();
117         groupAddressesRespondingSpec.clear();
118     }
119
120     @Override
121     protected void cancelReadFutures() {
122         for (GroupAddress groupAddress : readFutures.keySet()) {
123             readFutures.computeIfPresent(groupAddress, (k, v) -> {
124                 v.cancel(true);
125                 return null;
126             });
127         }
128     }
129
130     @FunctionalInterface
131     private interface ChannelFunction {
132         void apply(KNXChannelType channelType, Configuration configuration) throws KNXException;
133     }
134
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);
139             return;
140         }
141         withKNXType(channel, function);
142     }
143
144     private void withKNXType(Channel channel, ChannelFunction function) {
145         try {
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);
150         }
151     }
152
153     private void forAllChannels(ChannelFunction function) {
154         for (Channel channel : getThing().getChannels()) {
155             withKNXType(channel, function);
156         }
157     }
158
159     @Override
160     public void channelLinked(ChannelUID channelUID) {
161         if (!isControl(channelUID)) {
162             withKNXType(channelUID, (selector, configuration) -> {
163                 scheduleRead(selector, configuration);
164             });
165         }
166     }
167
168     @Override
169     protected void scheduleReadJobs() {
170         cancelReadFutures();
171         for (Channel channel : getThing().getChannels()) {
172             if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) {
173                 withKNXType(channel, (selector, configuration) -> {
174                     scheduleRead(selector, configuration);
175                 });
176             }
177         }
178     }
179
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());
185             }
186         }
187     }
188
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,
194                         TimeUnit.SECONDS);
195                 readFutures.put(groupAddress, future);
196             }
197         } else {
198             getScheduler().submit(() -> readDatapoint(groupAddress, dpt));
199         }
200     }
201
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);
206                 return;
207             }
208             Datapoint datapoint = new CommandDP(groupAddress, getThing().getUID().toString(), 0, dpt);
209             getClient().readDatapoint(datapoint);
210         }
211     }
212
213     @Override
214     public boolean listensTo(GroupAddress destination) {
215         return groupAddresses.contains(destination);
216     }
217
218     /** KNXIO remember controls, removeIf may be null */
219     @SuppressWarnings("null")
220     private void rememberRespondingSpec(OutboundSpec commandSpec, boolean add) {
221         GroupAddress ga = commandSpec.getGroupAddress();
222         if (ga != null) {
223             groupAddressesRespondingSpec.removeIf(spec -> spec.getGroupAddress().equals(ga));
224         }
225         if (add) {
226             groupAddressesRespondingSpec.add(commandSpec);
227         }
228         logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga,
229                 groupAddressesRespondingSpec.size(), add);
230     }
231
232     /** Handling commands triggered from openHAB */
233     @Override
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);
240             });
241         } else {
242             switch (channelUID.getId()) {
243                 case CHANNEL_RESET:
244                     if (address != null) {
245                         restart();
246                     }
247                     break;
248                 default:
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);
257                             }
258                         } else {
259                             logger.debug(
260                                     "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
261                                     channelUID, command, command.getClass().getSimpleName());
262                         }
263                     });
264                     break;
265             }
266         }
267     }
268
269     private boolean isControl(ChannelUID channelUID) {
270         ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID);
271         return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId());
272     }
273
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;
280     }
281
282     /** KNXIO */
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);
292                     }
293                     return false;
294                 }).findFirst();
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());
299                 }
300             });
301         }
302     }
303
304     /**
305      * KNXIO, extended with the ability to respond on "GroupValueRead" telegrams with "GroupValueResponse" telegram
306      */
307     @Override
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);
325                     }
326                 });
327             }
328         }
329     }
330
331     @Override
332     public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
333             byte[] asdu) {
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);
338     }
339
340     /**
341      * KNXIO, here value changes are set, coming from KNX OR openHAB.
342      */
343     @Override
344     public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
345             byte[] asdu) {
346         logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'",
347                 getThing().getUID(), source, destination);
348
349         for (Channel channel : getThing().getChannels()) {
350             withKNXType(channel, (selector, configuration) -> {
351                 InboundSpec listenSpec = selector.getListenSpec(configuration, destination);
352                 if (listenSpec != null) {
353                     logger.trace(
354                             "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
355                             getThing().getUID(), destination, channel.getUID());
356                     /**
357                      * Remember current KNXIO outboundSpec only if it is a control channel.
358                      */
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()),
363                                 asdu);
364                         if (type != null) {
365                             OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type);
366                             if (commandSpec != null) {
367                                 rememberRespondingSpec(commandSpec, true);
368                             }
369                         }
370                     }
371                     processDataReceived(destination, asdu, listenSpec, channel.getUID());
372                 }
373             });
374         }
375     }
376
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());
381             return;
382         }
383
384         Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT());
385         Type type = typeHelper.toType(datapoint, asdu);
386
387         if (type != null) {
388             if (isControl(channelUID)) {
389                 Channel channel = getThing().getChannel(channelUID.getId());
390                 Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY)
391                         : null;
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) -> {
398                             v.cancel(false);
399                             return null;
400                         });
401                     } else if (type instanceof IncreaseDecreaseType) {
402                         channelFutures.compute(channelUID, (k, v) -> {
403                             if (v != null) {
404                                 v.cancel(true);
405                             }
406                             return scheduler.scheduleWithFixedDelay(() -> postCommand(channelUID, (Command) type), 0,
407                                     frequency, TimeUnit.MILLISECONDS);
408                         });
409                     }
410                 } else {
411                     if (type instanceof Command) {
412                         logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address);
413                         postCommand(channelUID, (Command) type);
414                     }
415                 }
416             } else {
417                 if (type instanceof State && !(type instanceof UnDefType)) {
418                     updateState(channelUID, (State) type);
419                 }
420             }
421         } else {
422             String s = asduToHex(asdu);
423             logger.warn(
424                     "Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')",
425                     destination, datapoint, s);
426         }
427     }
428
429     private boolean isDPTSupported(@Nullable String dpt) {
430         return typeHelper.toTypeClass(dpt) != null;
431     }
432
433     private KNXChannelType getKNXChannelType(Channel channel) {
434         return KNXChannelTypes.getType(channel.getChannelTypeUID());
435     }
436 }