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.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
22 import java.util.Objects;
23 import java.util.Optional;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
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;
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;
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.
64 * @author Simon Kaufmann - Initial contribution and API
67 public class DeviceThingHandler extends AbstractKNXThingHandler {
69 private final Logger logger = LoggerFactory.getLogger(DeviceThingHandler.class);
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;
79 public DeviceThingHandler(Thing thing) {
84 public void initialize() {
86 DeviceConfig config = getConfigAs(DeviceConfig.class);
87 readInterval = config.getReadInterval();
88 initializeGroupAddresses();
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));
100 public void dispose() {
101 cancelChannelFutures();
106 private void cancelChannelFutures() {
107 for (ScheduledFuture<?> future : channelFutures.values()) {
108 if (!future.isDone()) {
112 channelFutures.clear();
115 private void freeGroupAdresses() {
116 groupAddresses.clear();
117 groupAddressesWriteBlockedOnce.clear();
118 groupAddressesRespondingSpec.clear();
122 protected void cancelReadFutures() {
123 for (ScheduledFuture<?> future : readFutures.values()) {
124 if (!future.isDone()) {
132 private interface ChannelFunction {
133 void apply(KNXChannelType channelType, Configuration configuration) throws KNXException;
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);
142 withKNXType(channel, function);
145 private void withKNXType(Channel channel, ChannelFunction function) {
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);
154 private void forAllChannels(ChannelFunction function) {
155 for (Channel channel : getThing().getChannels()) {
156 withKNXType(channel, function);
161 public void channelLinked(ChannelUID channelUID) {
162 if (!isControl(channelUID)) {
163 withKNXType(channelUID, (selector, configuration) -> {
164 scheduleRead(selector, configuration);
170 protected void scheduleReadJobs() {
172 for (Channel channel : getThing().getChannels()) {
173 if (isLinked(channel.getUID().getId()) && !isControl(channel.getUID())) {
174 withKNXType(channel, (selector, configuration) -> {
175 scheduleRead(selector, configuration);
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());
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,
196 readFutures.put(groupAddress, future);
199 getScheduler().submit(() -> readDatapoint(groupAddress, dpt));
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);
209 Datapoint datapoint = new CommandDP(groupAddress, getThing().getUID().toString(), 0, dpt);
210 getClient().readDatapoint(datapoint);
215 public boolean listensTo(GroupAddress destination) {
216 return groupAddresses.contains(destination);
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));
225 groupAddressesRespondingSpec.add(commandSpec);
227 logger.trace("rememberRespondingSpec handled commandSpec for '{}' size '{}' added '{}'", ga,
228 groupAddressesRespondingSpec.size(), add);
231 /** Handling commands triggered from openHAB */
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);
241 switch (channelUID.getId()) {
243 if (address != null) {
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);
259 "None of the configured GAs on channel '{}' could handle the command '{}' of type '{}'",
260 channelUID, command, command.getClass().getSimpleName());
268 private boolean isControl(ChannelUID channelUID) {
269 ChannelTypeUID channelTypeUID = getChannelTypeUID(channelUID);
270 return CONTROL_CHANNEL_TYPES.contains(channelTypeUID.getId());
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;
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);
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());
304 * KNXIO, extended with the ability to respond on "GroupValueRead" telegrams with "GroupValueResponse" telegram
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);
331 public void onGroupReadResponse(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
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);
340 * KNXIO, here value changes are set, coming from KNX OR openHAB.
343 public void onGroupWrite(AbstractKNXClient client, IndividualAddress source, GroupAddress destination,
345 logger.debug("onGroupWrite Thing '{}' received a GroupValueWrite telegram from '{}' for destination '{}'",
346 getThing().getUID(), source, destination);
348 for (Channel channel : getThing().getChannels()) {
349 withKNXType(channel, (selector, configuration) -> {
350 InboundSpec listenSpec = selector.getListenSpec(configuration, destination);
351 if (listenSpec != null) {
353 "onGroupWrite Thing '{}' processes a GroupValueWrite telegram for destination '{}' for channel '{}'",
354 getThing().getUID(), destination, channel.getUID());
356 * Remember current KNXIO outboundSpec only if it is a control channel.
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()),
364 OutboundSpec commandSpec = selector.getCommandSpec(configuration, typeHelper, type);
365 if (commandSpec != null) {
366 rememberRespondingSpec(commandSpec, true);
370 processDataReceived(destination, asdu, listenSpec, channel.getUID());
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());
383 Datapoint datapoint = new CommandDP(destination, getThing().getUID().toString(), 0, listenSpec.getDPT());
384 Type type = typeHelper.toType(datapoint, asdu);
387 if (isControl(channelUID)) {
388 Channel channel = getThing().getChannel(channelUID.getId());
389 Object repeat = channel != null ? channel.getConfiguration().get(KNXBindingConstants.REPEAT_FREQUENCY)
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);
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);
410 if (type instanceof Command) {
411 logger.trace("processDataReceived postCommand new value '{}' for GA '{}'", asdu, address);
412 postCommand(channelUID, (Command) type);
416 if (type instanceof State) {
417 updateState(channelUID, (State) type);
421 String s = asduToHex(asdu);
423 "Ignoring KNX bus data: couldn't transform to any Type (destination='{}', datapoint='{}', data='{}')",
424 destination, datapoint, s);
428 private boolean isDPTSupported(@Nullable String dpt) {
429 return typeHelper.toTypeClass(dpt) != null;
432 private KNXChannelType getKNXChannelType(Channel channel) {
433 return KNXChannelTypes.getType(channel.getChannelTypeUID());