2 * Copyright (c) 2010-2024 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.insteon.internal.device;
15 import java.lang.reflect.InvocationTargetException;
16 import java.util.HashMap;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.insteon.internal.message.FieldException;
22 import org.openhab.binding.insteon.internal.message.Msg;
23 import org.openhab.binding.insteon.internal.utils.Utils;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
28 * Does preprocessing of messages to decide which handler should be called.
30 * @author Bernd Pfrommer - Initial contribution
31 * @author Rob Nielsen - Port to openHAB 2 insteon binding
34 public abstract class MessageDispatcher {
35 private static final Logger logger = LoggerFactory.getLogger(MessageDispatcher.class);
37 DeviceFeature feature;
39 Map<String, String> parameters = new HashMap<>();
44 * @param f DeviceFeature to which this MessageDispatcher belongs
46 MessageDispatcher(DeviceFeature f) {
50 public void setParameters(@Nullable Map<String, String> map) {
55 * Generic handling of incoming ALL LINK messages
57 * @param msg the message received
58 * @return true if the message was handled by this function
60 protected boolean handleAllLinkMessage(Msg msg) {
61 if (!msg.isAllLink()) {
65 InsteonAddress a = msg.getAddress("toAddress");
66 // ALL_LINK_BROADCAST and ALL_LINK_CLEANUP
67 // have a valid Command1 field
68 // but the CLEANUP_SUCCESS (of type ALL_LINK_BROADCAST!)
69 // message has cmd1 = 0x06 and the cmd as the
70 // high byte of the toAddress.
71 byte cmd1 = msg.getByte("command1");
72 if (!msg.isCleanup() && cmd1 == 0x06) {
73 cmd1 = a.getHighByte();
75 // For ALL_LINK_BROADCAST messages, the group is
76 // in the low byte of the toAddress. For direct
77 // ALL_LINK_CLEANUP, it is in Command2
79 int group = (msg.isCleanup() ? msg.getByte("command2") : a.getLowByte()) & 0xff;
80 MessageHandler h = feature.getMsgHandlers().get(cmd1 & 0xFF);
82 logger.debug("msg is not for this feature");
85 if (!h.isDuplicate(msg)) {
86 if (h.matchesGroup(group) && h.matches(msg)) {
87 logger.debug("{}:{}->{} cmd1:{} group {}/{}", feature.getDevice().getAddress(), feature.getName(),
88 h.getClass().getSimpleName(), Utils.getHexByte(cmd1), group, h.getGroup());
89 h.handleMessage(group, cmd1, msg, feature);
91 logger.debug("message ignored because matches group: {} matches filter: {}", h.matchesGroup(group),
95 logger.debug("message ignored as duplicate. Matches group: {} matches filter: {}",
96 h.matchesGroup(group), h.matches(msg));
98 } catch (FieldException e) {
99 logger.warn("couldn't parse ALL_LINK message: {}", msg, e);
105 * Checks if this message is in response to previous query by this feature
110 @SuppressWarnings("PMD.CompareObjectsWithEquals")
111 boolean isMyDirectAck(Msg msg) {
112 return msg.isAckOfDirect() && (feature.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING)
113 && feature.getDevice().getFeatureQueried() == feature;
119 * @param msg Message to dispatch
120 * @return true if this message was found to be a reply to a direct message,
121 * and was claimed by one of the handlers
123 public abstract boolean dispatch(Msg msg);
127 // ------------ implementations of MessageDispatchers start here ------------------
131 public static class DefaultDispatcher extends MessageDispatcher {
132 DefaultDispatcher(DeviceFeature f) {
137 public boolean dispatch(Msg msg) {
140 boolean isConsumed = false;
143 cmd = msg.getByte("Cmd");
144 cmd1 = msg.getByte("command1");
145 } catch (FieldException e) {
146 logger.debug("no command found, dropping msg {}", msg);
149 if (msg.isAllLinkCleanupAckOrNack()) {
150 // Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
151 // in response to a direct status query message
154 if (handleAllLinkMessage(msg)) {
157 if (msg.isAckOfDirect()) {
158 // in the case of direct ack, the cmd1 code is useless.
159 // you have to know what message was sent before to
160 // interpret the reply message
161 if (isMyDirectAck(msg)) {
162 logger.debug("{}:{} DIRECT_ACK: q:{} cmd: {}", feature.getDevice().getAddress(), feature.getName(),
163 feature.getQueryStatus(), cmd);
166 // must be a reply to our message, tweak the cmd1 code!
167 logger.debug("changing key to 0x19 for msg {}", msg);
168 key = 0x19; // we have installed a handler under that command number
174 if (key != -1 || feature.isStatusFeature()) {
175 MessageHandler h = feature.getMsgHandlers().get(key);
177 h = feature.getDefaultMsgHandler();
179 if (h.matches(msg)) {
181 logger.debug("{}:{}->{} DIRECT", feature.getDevice().getAddress(), feature.getName(),
182 h.getClass().getSimpleName());
184 h.handleMessage(-1, cmd1, msg, feature);
188 feature.setQueryStatus(DeviceFeature.QueryStatus.QUERY_ANSWERED);
189 logger.debug("defdisp: {}:{} set status to: {}", feature.getDevice().getAddress(), feature.getName(),
190 feature.getQueryStatus());
196 public static class DefaultGroupDispatcher extends MessageDispatcher {
197 DefaultGroupDispatcher(DeviceFeature f) {
202 public boolean dispatch(Msg msg) {
205 boolean isConsumed = false;
208 cmd = msg.getByte("Cmd");
209 cmd1 = msg.getByte("command1");
210 } catch (FieldException e) {
211 logger.debug("no command found, dropping msg {}", msg);
214 if (msg.isAllLinkCleanupAckOrNack()) {
215 // Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
216 // in response to a direct status query message
219 if (handleAllLinkMessage(msg)) {
222 if (msg.isAckOfDirect()) {
223 // in the case of direct ack, the cmd1 code is useless.
224 // you have to know what message was sent before to
225 // interpret the reply message
226 if (isMyDirectAck(msg)) {
227 logger.debug("{}:{} qs:{} cmd: {}", feature.getDevice().getAddress(), feature.getName(),
228 feature.getQueryStatus(), cmd);
231 // must be a reply to our message, tweak the cmd1 code!
232 logger.debug("changing key to 0x19 for msg {}", msg);
233 key = 0x19; // we have installed a handler under that command number
240 for (DeviceFeature f : feature.getConnectedFeatures()) {
241 MessageHandler h = f.getMsgHandlers().get(key);
243 h = f.getDefaultMsgHandler();
245 if (h.matches(msg)) {
247 logger.debug("{}:{}->{} DIRECT", f.getDevice().getAddress(), f.getName(),
248 h.getClass().getSimpleName());
250 h.handleMessage(-1, cmd1, msg, f);
256 feature.setQueryStatus(DeviceFeature.QueryStatus.QUERY_ANSWERED);
257 logger.debug("{}:{} set status to: {}", feature.getDevice().getAddress(), feature.getName(),
258 feature.getQueryStatus());
264 public static class PollGroupDispatcher extends MessageDispatcher {
265 PollGroupDispatcher(DeviceFeature f) {
270 public boolean dispatch(Msg msg) {
271 if (msg.isAllLinkCleanupAckOrNack()) {
272 // Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
273 // in response to a direct status query message
276 if (handleAllLinkMessage(msg)) {
279 if (msg.isAckOfDirect()) {
280 boolean isMyAck = isMyDirectAck(msg);
282 logger.debug("{}:{} got poll ACK", feature.getDevice().getAddress(), feature.getName());
286 return (false); // not a direct ack, so we didn't consume it either
290 public static class SimpleDispatcher extends MessageDispatcher {
291 SimpleDispatcher(DeviceFeature f) {
296 public boolean dispatch(Msg msg) {
299 if (handleAllLinkMessage(msg)) {
302 if (msg.isAllLinkCleanupAckOrNack()) {
303 // Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
304 // in response to a direct status query message
307 cmd1 = msg.getByte("command1");
308 } catch (FieldException e) {
309 logger.debug("no cmd1 found, dropping msg {}", msg);
312 boolean isConsumed = isMyDirectAck(msg);
313 int key = (cmd1 & 0xFF);
314 MessageHandler h = feature.getMsgHandlers().get(key);
316 h = feature.getDefaultMsgHandler();
318 if (h.matches(msg)) {
319 logger.trace("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
320 h.getClass().getSimpleName(), msg);
321 h.handleMessage(-1, cmd1, msg, feature);
327 public static class X10Dispatcher extends MessageDispatcher {
328 X10Dispatcher(DeviceFeature f) {
333 public boolean dispatch(Msg msg) {
335 byte rawX10 = msg.getByte("rawX10");
336 int cmd = (rawX10 & 0x0f);
337 MessageHandler h = feature.getMsgHandlers().get(cmd);
339 h = feature.getDefaultMsgHandler();
341 logger.debug("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
342 h.getClass().getSimpleName(), msg);
343 if (h.matches(msg)) {
344 h.handleMessage(-1, (byte) cmd, msg, feature);
346 } catch (FieldException e) {
347 logger.warn("error parsing {}: ", msg, e);
353 public static class PassThroughDispatcher extends MessageDispatcher {
354 PassThroughDispatcher(DeviceFeature f) {
359 public boolean dispatch(Msg msg) {
360 MessageHandler h = feature.getDefaultMsgHandler();
361 if (h.matches(msg)) {
362 logger.trace("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
363 h.getClass().getSimpleName(), msg);
364 h.handleMessage(-1, (byte) 0x01, msg, feature);
371 * Drop all incoming messages silently
373 public static class NoOpDispatcher extends MessageDispatcher {
374 NoOpDispatcher(DeviceFeature f) {
379 public boolean dispatch(Msg msg) {
385 * Factory method for creating a dispatcher of a given name using java reflection
387 * @param name the name of the dispatcher to create
389 * @param f the feature for which to create the dispatcher
390 * @return the handler which was created
393 public static <T extends MessageDispatcher> T makeHandler(String name, @Nullable Map<String, String> params,
395 String cname = MessageDispatcher.class.getName() + "$" + name;
397 Class<?> c = Class.forName(cname);
398 @SuppressWarnings("unchecked")
399 Class<? extends T> dc = (Class<? extends T>) c;
401 T ch = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
402 ch.setParameters(params);
404 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
405 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
406 logger.warn("error trying to create dispatcher: {}", name, e);