]> git.basschouten.com Git - openhab-addons.git/blob
68d5c74b72e4f21bcaaa5ae9c1bb833778878353
[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.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Optional;
24 import java.util.Set;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.knx.internal.KNXBindingConstants;
31 import org.openhab.binding.knx.internal.KNXTypeMapper;
32 import org.openhab.binding.knx.internal.channel.KNXChannelType;
33 import org.openhab.binding.knx.internal.channel.KNXChannelTypes;
34 import org.openhab.binding.knx.internal.client.AbstractKNXClient;
35 import org.openhab.binding.knx.internal.client.InboundSpec;
36 import org.openhab.binding.knx.internal.client.OutboundSpec;
37 import org.openhab.binding.knx.internal.config.DeviceConfig;
38 import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
39 import org.openhab.core.config.core.Configuration;
40 import org.openhab.core.library.types.IncreaseDecreaseType;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.type.ChannelTypeUID;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.Type;
49 import org.openhab.core.types.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import tuwien.auto.calimero.GroupAddress;
54 import tuwien.auto.calimero.IndividualAddress;
55 import tuwien.auto.calimero.KNXException;
56 import tuwien.auto.calimero.KNXFormatException;
57 import tuwien.auto.calimero.datapoint.CommandDP;
58 import tuwien.auto.calimero.datapoint.Datapoint;
59
60 /**
61  * The {@link DeviceThingHandler} is responsible for handling commands and state updates sent to and received from the
62  * bus and updating the channels correspondingly.
63  *
64  * @author Simon Kaufmann - Initial contribution and API
65  */
66 @NonNullByDefault
67 public class DeviceThingHandler extends AbstractKNXThingHandler {
68
69     private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class);
70
71     private final KNXTypeMapper typeHelper = new KNXCoreTypeMapper();
72     private final Set<GroupAddress> groupAddresses = new HashSet<>();
73     private final Set<GroupAddress> groupAddressesWriteBlockedOnce = new HashSet<>();
74     private final Set<OutboundSpec> groupAddressesRespondingSpec = new HashSet<>();
75     private final Map<GroupAddress, ScheduledFuture<?>> readFutures = new HashMap<>();
76     private final Map<ChannelUID, ScheduledFuture<?>> channelFutures = new HashMap<>();
77     private int readInterval;
78
79     public DeviceThingHandler(Thing thing) {
80         super(thing);
81     }
82
83     @Override
84     public void initialize() {
85         super.initialize();
86         DeviceConfig config = getConfigAs(DeviceConfig.class);
87         readInterval = config.getReadInterval();
88         initializeGroupAddresses();
89     }
90
91     private void initializeGroupAddresses() {
92         forAllChannels((selector, channelConfiguration) -> {
93             groupAddresses.addAll(selector.getReadAddresses(channelConfiguration));
94             groupAddresses.addAll(selector.getWriteAddresses(channelConfiguration));
95             groupAddresses.addAll(selector.getListenAddresses(channelConfiguration));
96         });
97     }
98
99     @Override
100     public void dispose() {
101         cancelChannelFutures();
102         freeGroupAdresses();
103         super.dispose();
104     }
105
106     private void cancelChannelFutures() {
107         for (ScheduledFuture<?> future : channelFutures.values()) {
108             if (!future.isDone()) {
109                 future.cancel(true);
110             }
111         }
112         channelFutures.clear();
113     }
114
115     private void freeGroupAdresses() {
116         groupAddresses.clear();
117         groupAddressesWriteBlockedOnce.clear();
118         groupAddressesRespondingSpec.clear();
119     }
120
121     @Override
122     protected void cancelReadFutures() {
123         for (ScheduledFuture<?> future : readFutures.values()) {
124             if (!future.isDone()) {
125                 future.cancel(true);
126             }
127         }
128         readFutures.clear();
129     }
130
131     @FunctionalInterface
132     private interface ChannelFunction {
133         void apply(KNXChannelType channelType, Configuration configuration) throws KNXException;
134     }
135
136     private void withKNXType(ChannelUID channelUID, ChannelFunction function) {
137         Channel channel = getThing().getChannel(channelUID.getId());
138         if (channel == null) {
139             logger.warn("Channel '{}' does not exist", channelUID);
140             return;
141         }
142         withKNXType(channel, function);
143     }
144
145     private void withKNXType(Channel channel, ChannelFunction function) {
146         try {
147             KNXChannelType selector = getKNXChannelType(channel);
148             function.apply(selector, channel.getConfiguration());
149         } catch (KNXException e) {
150             logger.warn("An error occurred on channel {}: {}", channel.getUID(), e.getMessage(), e);
151         }
152     }
153
154     private void forAllChannels(ChannelFunction function) {
155         for (Channel channel : getThing().getChannels()) {
156             withKNXType(channel, function);
157         }
158     }
159
160     @Override
161     public void channelLinked(ChannelUID channelUID) {
162         if (!isControl(channelUID)) {
163             withKNXType(channelUID, (selector, configuration) -> {
164                 scheduleRead(selector, configuration);
165             });
166         }
167     }
168
169     @Override
170     protected void scheduleReadJobs() {
171         cancelReadFutures();
172         for (Channel channel : getThing().getChannels()) {
173             if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) {
174                 withKNXType(channel, (selector, configuration) -> {
175                     scheduleRead(selector, configuration);
176                 });
177             }
178         }
179     }
180
181     private void scheduleRead(KNXChannelType selector, Configuration configuration) throws KNXFormatException {
182         List<InboundSpec> readSpecs = selector.getReadSpec(configuration);
183         for (InboundSpec readSpec : readSpecs) {
184             for (GroupAddress groupAddress : readSpec.getGroupAddresses()) {
185                 scheduleReadJob(groupAddress, readSpec.getDPT());
186             }
187         }
188     }
189
190     private void scheduleReadJob(GroupAddress groupAddress, String dpt) {
191         if (readInterval > 0) {
192             ScheduledFuture<?> future = readFutures.get(groupAddress);
193             if (future == null || future.isDone() || future.isCancelled()) {
194                 future = getScheduler().scheduleWithFixedDelay(() -> readDatapoint(groupAddress, dpt), 0, readInterval,
195                         TimeUnit.SECONDS);
196                 readFutures.put(groupAddress, future);
197             }
198         } else {
199             getScheduler().submit(() -> readDatapoint(groupAddress, dpt));
200         }
201     }
202
203     private void readDatapoint(GroupAddress groupAddress, String dpt) {
204         if (getClient().isConnected()) {
205             if (!isDPTSupported(dpt)) {
206                 logger.warn("DPT '{}' is not supported by the KNX binding", dpt);
207                 return;
208             }
209             Datapoint datapoint = new CommandDP(groupAddress, getThing().getUID().toString(), 0, dpt);
210             getClient().readDatapoint(datapoint);
211         }
212     }
213
214     @Override
215     public boolean listensTo(GroupAddress destination) {
216         return groupAddresses.contains(destination);
217     }
218
219     /** KNXIO remember controls, removeIf may be null */
220     @SuppressWarnings("null")
221     private void rememberRespondingSpec(OutboundSpec commandSpec, boolean add) {
222         GroupAddress ga = commandSpec.getGroupAddress();
223         groupAddressesRespondingSpec.removeIf(spec -> spec.getGroupAddress().equals(ga));
224         if (add) {
225             groupAddressesRespondingSpec.add(commandSpec);
226         }
227         logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga,
228                 groupAddressesRespondingSpec.size(), add);
229     }
230
231     /** Handling commands triggered from openHAB */
232     @Override
233     public void handleCommand(ChannelUID channelUID, Command command) {
234         logger.trace("Handling command '{}' for channel '{}'", command, channelUID);
235         if (command instanceof RefreshType && !isControl(channelUID)) {
236             logger.debug("Refreshing channel '{}'", channelUID);
237             withKNXType(channelUID, (selector, configuration) -> {
238                 scheduleRead(selector, configuration);
239             });
240         } else {
241             switch (channelUID.getId()) {
242                 case CHANNEL_RESET:
243                     if (address != null) {
244                         restart();
245                     }
246                     break;
247                 default:
248                     withKNXType(channelUID, (selector, channelConfiguration) -> {
249                         OutboundSpec commandSpec = selector.getCommandSpec(channelConfiguration, typeHelper, command);
250                         // only send GroupValueWrite to KNX if GA is not blocked once
251                         if (commandSpec != null
252                                 && !groupAddressesWriteBlockedOnce.remove(commandSpec.getGroupAddress())) {
253                             getClient().writeToKNX(commandSpec);
254                             if (isControl(channelUID)) {
255                                 rememberRespondingSpec(commandSpec, true);
256                             }
257                         } else {
258                             logger.debug(
259                                     "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
260                                     channelUID, command, command.getClass().getSimpleName());
261                         }
262                     });
263                     break;
264             }
265         }
266     }
267
268     private boolean isControl(ChannelUID channelUID) {
269         ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID);
270         return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId());
271     }
272
273     private ChannelTypeUID getChannelTypeUID(ChannelUID channelUID) {
274         Channel channel = getThing().getChannel(channelUID.getId());
275         Objects.requireNonNull(channel);
276         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
277         Objects.requireNonNull(channelTypeUID);
278         return channelTypeUID;
279     }
280
281     /** KNXIO */
282     private void sendGroupValueResponse(Channel channel, GroupAddress destination) {
283         Set<GroupAddress> rsa = getKNXChannelType(channel).getWriteAddresses(channel.getConfiguration());
284         if (!rsa.isEmpty()) {
285             logger.trace("onGroupRead size '{}'", rsa.size());
286             withKNXType(channel, (selector, configuration) -> {
287                 Optional<OutboundSpec> os = groupAddressesRespondingSpec.stream().filter(spec -> {
288                     GroupAddress groupAddress = spec.getGroupAddress();
289                     if (groupAddress != null) {
290                         return groupAddress.equals(destination);
291                     }
292                     return false;
293                 }).findFirst();
294                 if (os.isPresent()) {
295                     logger.trace("onGroupRead respondToKNX '{}'", os.get().getGroupAddress());
296                     /** KNXIO: sending real "GroupValueResponse" to the KNX bus. */
297                     getClient().respondToKNX(os.get());
298                 }
299             });
300         }
301     }
302
303     /**
304      * KNXIO, extended with the ability to respond on "GroupValueRead" telegrams with "GroupValueResponse" telegram
305      */
306     @Override
307     public void onGroupRead(AbstractKNXClient client, IndividualAddress source, GroupAddress destination, byte[] asdu) {
308         logger.trace("onGroupRead Thing '{}' received a GroupValueRead telegram from '{}' for destination '{}'",
309                 getThing().getUID(), source, destination);
310         for (Channel channel : getThing().getChannels()) {
311             if (isControl(channel.getUID())) {
312                 withKNXType(channel, (selector, configuration) -> {
313                     OutboundSpec responseSpec = selector.getResponseSpec(configuration, destination,
314                             RefreshType.REFRESH);
315                     if (responseSpec != null) {
316                         logger.trace("onGroupRead isControl -> postCommand");
317                         // This event should be sent to KNX as GroupValueResponse immediately.
318                         sendGroupValueResponse(channel, destination);
319                         // Send REFRESH to openHAB to get this event for scripting with postCommand
320                         // and remember to ignore/block this REFRESH to be sent back to KNX as GroupValueWrite after
321                         // postCommand is done!
322                         groupAddressesWriteBlockedOnce.add(destination);
323                         postCommand(channel.getUID().getId(), RefreshType.REFRESH);
324                     }
325                 });
326             }
327         }
328     }
329
330     @Override
331     public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
332             byte[] asdu) {
333         // GroupValueResponses are treated the same as GroupValueWrite telegrams
334         logger.trace("onGroupReadResponse Thing '{}' processes a GroupValueResponse telegram for destination '{}'",
335                 getThing().getUID(), destination);
336         onGroupWrite(client, source, destination, asdu);
337     }
338
339     /**
340      * KNXIO, here value changes are set, coming from KNX OR openHAB.
341      */
342     @Override
343     public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
344             byte[] asdu) {
345         logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'",
346                 getThing().getUID(), source, destination);
347
348         for (Channel channel : getThing().getChannels()) {
349             withKNXType(channel, (selector, configuration) -> {
350                 InboundSpec listenSpec = selector.getListenSpec(configuration, destination);
351                 if (listenSpec != null) {
352                     logger.trace(
353                             "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
354                             getThing().getUID(), destination, channel.getUID());
355                     /**
356                      * Remember current KNXIO outboundSpec only if it is a control channel.
357                      */
358                     if (isControl(channel.getUID())) {
359                         logger.trace("onGroupWrite isControl");
360                         Type type = typeHelper.toType(
361                                 new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT()),
362                                 asdu);
363                         if (type != null) {
364                             OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type);
365                             if (commandSpec != null) {
366                                 rememberRespondingSpec(commandSpec, true);
367                             }
368                         }
369                     }
370                     processDataReceived(destination, asdu, listenSpec, channel.getUID());
371                 }
372             });
373         }
374     }
375
376     private void processDataReceived(GroupAddress destination, byte[] asdu, InboundSpec listenSpec,
377             ChannelUID channelUID) {
378         if (!isDPTSupported(listenSpec.getDPT())) {
379             logger.warn("DPT '{}' is not supported by the KNX binding.", listenSpec.getDPT());
380             return;
381         }
382
383         Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT());
384         Type type = typeHelper.toType(datapoint, asdu);
385
386         if (type != null) {
387             if (isControl(channelUID)) {
388                 Channel channel = getThing().getChannel(channelUID.getId());
389                 Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY)
390                         : null;
391                 int frequency = repeat != null ? ((BigDecimal) repeat).intValue() : 0;
392                 if (KNXBindingConstants.CHANNEL_DIMMER_CONTROL.equals(getChannelTypeUID(channelUID).getId())
393                         && (type instanceof UnDefType || type instanceof IncreaseDecreaseType) && frequency > 0) {
394                     // continuous dimming by the binding
395                     if (UnDefType.UNDEF.equals(type)) {
396                         ScheduledFuture<?> future = channelFutures.remove(channelUID);
397                         if (future != null) {
398                             future.cancel(false);
399                         }
400                     } else if (type instanceof IncreaseDecreaseType) {
401                         ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(() -> {
402                             postCommand(channelUID, (Command) type);
403                         }, 0, frequency, TimeUnit.MILLISECONDS);
404                         ScheduledFuture<?> previousFuture = channelFutures.put(channelUID, future);
405                         if (previousFuture != null) {
406                             previousFuture.cancel(true);
407                         }
408                     }
409                 } else {
410                     if (type instanceof Command) {
411                         logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address);
412                         postCommand(channelUID, (Command) type);
413                     }
414                 }
415             } else {
416                 if (type instanceof State && !(type instanceof UnDefType)) {
417                     updateState(channelUID, (State) type);
418                 }
419             }
420         } else {
421             String s = asduToHex(asdu);
422             logger.warn(
423                     "Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')",
424                     destination, datapoint, s);
425         }
426     }
427
428     private boolean isDPTSupported(@Nullable String dpt) {
429         return typeHelper.toTypeClass(dpt) != null;
430     }
431
432     private KNXChannelType getKNXChannelType(Channel channel) {
433         return KNXChannelTypes.getType(channel.getChannelTypeUID());
434     }
435 }