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.command;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.io.PrintStream;
19 import java.text.SimpleDateFormat;
20 import java.util.Arrays;
21 import java.util.Date;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map.Entry;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.openhab.binding.insteon.internal.InsteonBindingConstants;
31 import org.openhab.binding.insteon.internal.device.InsteonAddress;
32 import org.openhab.binding.insteon.internal.device.InsteonScene;
33 import org.openhab.binding.insteon.internal.device.X10Address;
34 import org.openhab.binding.insteon.internal.device.X10Device;
35 import org.openhab.binding.insteon.internal.transport.PortListener;
36 import org.openhab.binding.insteon.internal.transport.message.FieldException;
37 import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
38 import org.openhab.binding.insteon.internal.transport.message.Msg;
39 import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
40 import org.openhab.binding.insteon.internal.transport.message.MsgDefinitionRegistry;
41 import org.openhab.binding.insteon.internal.utils.HexUtils;
42 import org.openhab.core.io.console.Console;
43 import org.openhab.core.io.console.StringsCompleter;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
49 * The {@link DebugCommand} represents an Insteon console debug command
51 * @author Jeremy Setton - Initial contribution
54 public class DebugCommand extends InsteonCommand implements PortListener {
55 private static final String NAME = "debug";
56 private static final String DESCRIPTION = "Insteon debug commands";
58 private static final String LIST_MONITORED = "listMonitored";
59 private static final String START_MONITORING = "startMonitoring";
60 private static final String STOP_MONITORING = "stopMonitoring";
61 private static final String SEND_BROADCAST_MESSAGE = "sendBroadcastMessage";
62 private static final String SEND_STANDARD_MESSAGE = "sendStandardMessage";
63 private static final String SEND_EXTENDED_MESSAGE = "sendExtendedMessage";
64 private static final String SEND_EXTENDED_2_MESSAGE = "sendExtended2Message";
65 private static final String SEND_X10_MESSAGE = "sendX10Message";
66 private static final String SEND_IM_MESSAGE = "sendIMMessage";
68 private static final List<String> SUBCMDS = List.of(LIST_MONITORED, START_MONITORING, STOP_MONITORING,
69 SEND_BROADCAST_MESSAGE, SEND_STANDARD_MESSAGE, SEND_EXTENDED_MESSAGE, SEND_EXTENDED_2_MESSAGE,
70 SEND_X10_MESSAGE, SEND_IM_MESSAGE);
72 private static final String ALL_OPTION = "--all";
74 private static final String MSG_EVENTS_FILE_PREFIX = "messageEvents";
76 private static enum MessageType {
82 private final Logger logger = LoggerFactory.getLogger(DebugCommand.class);
84 private boolean monitoring = false;
85 private boolean monitorAllDevices = false;
86 private Set<InsteonAddress> monitoredAddresses = new HashSet<>();
88 public DebugCommand(InsteonCommandExtension commandExtension) {
89 super(NAME, DESCRIPTION, commandExtension);
93 public List<String> getUsages() {
94 return List.of(buildCommandUsage(LIST_MONITORED, "list monitored device(s)"),
95 buildCommandUsage(START_MONITORING + " " + ALL_OPTION + "|<address>",
96 "start logging message events for device(s) in separate file(s)"),
97 buildCommandUsage(STOP_MONITORING + " " + ALL_OPTION + "|<address>",
98 "stop logging message events for device(s) in separate file(s)"),
99 buildCommandUsage(SEND_BROADCAST_MESSAGE + " <group> <cmd1> <cmd2>",
100 "send an Insteon broadcast message to a group"),
101 buildCommandUsage(SEND_STANDARD_MESSAGE + " <address> <cmd1> <cmd2>",
102 "send an Insteon standard message to a device"),
103 buildCommandUsage(SEND_EXTENDED_MESSAGE + " <address> <cmd1> <cmd2> [<data1> ... <data13>]",
104 "send an Insteon extended message with standard crc to a device"),
105 buildCommandUsage(SEND_EXTENDED_2_MESSAGE + " <address> <cmd1> <cmd2> [<data1> ... <data12>]",
106 "send an Insteon extended message with a two-byte crc to a device"),
107 buildCommandUsage(SEND_X10_MESSAGE + " <address> <cmd>", "send an X10 message to a device"),
108 buildCommandUsage(SEND_IM_MESSAGE + " <name> [<data1> <data2> ...]",
109 "send an IM message to the modem"));
113 public void execute(String[] args, Console console) {
114 if (args.length == 0) {
121 if (args.length == 1) {
122 listMonitoredDevices(console);
124 printUsage(console, args[0]);
127 case START_MONITORING:
128 if (args.length == 2) {
129 startMonitoring(console, args[1]);
131 printUsage(console, args[0]);
134 case STOP_MONITORING:
135 if (args.length == 2) {
136 stopMonitoring(console, args[1]);
138 printUsage(console, args[0]);
141 case SEND_BROADCAST_MESSAGE:
142 if (args.length == 4) {
143 sendBroadcastMessage(console, args);
145 printUsage(console, args[0]);
148 case SEND_STANDARD_MESSAGE:
149 if (args.length == 4) {
150 sendDirectMessage(console, MessageType.STANDARD, args);
152 printUsage(console, args[0]);
155 case SEND_EXTENDED_MESSAGE:
156 if (args.length >= 4 && args.length <= 17) {
157 sendDirectMessage(console, MessageType.EXTENDED, args);
159 printUsage(console, args[0]);
162 case SEND_EXTENDED_2_MESSAGE:
163 if (args.length >= 4 && args.length <= 16) {
164 sendDirectMessage(console, MessageType.EXTENDED_2, args);
166 printUsage(console, args[0]);
169 case SEND_X10_MESSAGE:
170 if (args.length == 3) {
171 sendX10Message(console, args);
173 printUsage(console, args[0]);
176 case SEND_IM_MESSAGE:
177 if (args.length >= 2) {
178 sendIMMessage(console, args);
180 printUsage(console, args[0]);
184 console.println("Unknown command '" + args[0] + "'");
191 public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
192 List<String> strings = List.of();
193 if (cursorArgumentIndex == 0) {
195 } else if (cursorArgumentIndex == 1) {
197 case START_MONITORING:
198 case STOP_MONITORING:
199 strings = Stream.concat(Stream.of(ALL_OPTION),
200 getModem().getDB().getDevices().stream().map(InsteonAddress::toString)).toList();
202 case SEND_BROADCAST_MESSAGE:
203 strings = getModem().getDB().getBroadcastGroups().stream().map(String::valueOf).toList();
205 case SEND_STANDARD_MESSAGE:
206 case SEND_EXTENDED_MESSAGE:
207 case SEND_EXTENDED_2_MESSAGE:
208 strings = getModem().getDB().getDevices().stream().map(InsteonAddress::toString).toList();
210 case SEND_X10_MESSAGE:
211 strings = getModem().getX10Devices().stream().map(X10Device::getAddress).map(X10Address::toString)
214 case SEND_IM_MESSAGE:
215 strings = MsgDefinitionRegistry.getInstance().getDefinitions().entrySet().stream()
216 .filter(entry -> entry.getValue().getDirection() == Direction.TO_MODEM).map(Entry::getKey)
222 return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
226 public void disconnected() {
231 public void messageReceived(Msg msg) {
233 InsteonAddress address = msg.getInsteonAddress(msg.isReply() ? "toAddress" : "fromAddress");
234 if (monitorAllDevices || monitoredAddresses.contains(address)) {
235 logMessageEvent(address, msg);
237 } catch (FieldException ignored) {
238 // ignore message with no address field
243 public void messageSent(Msg msg) {
245 InsteonAddress address = msg.getInsteonAddress("toAddress");
246 if (monitorAllDevices || monitoredAddresses.contains(address)) {
247 logMessageEvent(address, msg);
249 } catch (FieldException ignored) {
250 // ignore message with no address field
254 private String getMsgEventsFileName(String address) {
255 return MSG_EVENTS_FILE_PREFIX + "-" + address.replace(".", "") + ".log";
258 private String getMsgEventsFilePath(String address) {
259 return InsteonBindingConstants.BINDING_DATA_DIR + File.separator + getMsgEventsFileName(address);
262 private void clearMonitorFiles(String address) {
263 File folder = new File(InsteonBindingConstants.BINDING_DATA_DIR);
264 String prefix = ALL_OPTION.equals(address) ? MSG_EVENTS_FILE_PREFIX : getMsgEventsFileName(address);
266 if (folder.isDirectory()) {
267 Arrays.asList(folder.listFiles()).stream().filter(file -> file.getName().startsWith(prefix))
268 .forEach(File::delete);
272 private void logMessageEvent(InsteonAddress address, Msg msg) {
273 String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
274 String pathname = getMsgEventsFilePath(address.toString());
277 File file = new File(pathname);
278 File parent = file.getParentFile();
279 if (parent == null) {
280 throw new IOException(pathname + " does not name a parent directory");
283 file.createNewFile();
285 PrintStream ps = new PrintStream(new FileOutputStream(file, true));
286 ps.println(timestamp + " " + msg.toString());
288 } catch (IOException e) {
289 logger.warn("failed to write to message event file", e);
293 private void listMonitoredDevices(Console console) {
294 String addresses = monitoredAddresses.stream().map(InsteonAddress::toString).collect(Collectors.joining(", "));
295 if (!addresses.isEmpty()) {
296 console.println("The monitored device(s) are: " + addresses);
297 } else if (monitorAllDevices) {
298 console.println("All devices are monitored.");
300 console.println("Not monitoring any devices.");
304 private void startMonitoring(Console console, String address) {
305 if (ALL_OPTION.equals(address)) {
306 if (!monitorAllDevices) {
307 monitorAllDevices = true;
308 monitoredAddresses.clear();
309 console.println("Started monitoring all devices.");
310 console.println("Message events logged in " + InsteonBindingConstants.BINDING_DATA_DIR);
311 clearMonitorFiles(address);
313 console.println("Already monitoring all devices.");
315 } else if (InsteonAddress.isValid(address)) {
316 if (monitorAllDevices) {
317 console.println("Already monitoring all devices.");
318 } else if (monitoredAddresses.add(new InsteonAddress(address))) {
319 console.println("Started monitoring the device " + address + ".");
320 console.println("Message events logged in " + getMsgEventsFilePath(address));
321 clearMonitorFiles(address);
323 console.println("Already monitoring the device " + address + ".");
326 console.println("Invalid device address" + address + ".");
331 getModem().getPort().registerListener(this);
336 private void stopMonitoring(Console console, String address) {
338 console.println("Not monitoring any devices.");
342 if (ALL_OPTION.equals(address)) {
343 if (monitorAllDevices) {
344 monitorAllDevices = false;
345 console.println("Stopped monitoring all devices.");
347 console.println("Not monitoring all devices.");
349 } else if (InsteonAddress.isValid(address)) {
350 if (monitorAllDevices) {
351 console.println("Not monitoring individual devices.");
352 } else if (monitoredAddresses.remove(new InsteonAddress(address))) {
353 console.println("Stopped monitoring the device " + address + ".");
355 console.println("Not monitoring the device " + address + ".");
359 console.println("Invalid address device address " + address + ".");
363 if (!monitorAllDevices && monitoredAddresses.isEmpty()) {
364 getModem().getPort().unregisterListener(this);
369 private void sendBroadcastMessage(Console console, String[] args) {
370 if (!InsteonScene.isValidGroup(args[1])) {
371 console.println("Invalid group argument: " + args[1]);
372 } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
373 console.println("Invalid hex argument(s).");
374 } else if (!getModem().getDB().isComplete()) {
375 console.println("Not ready to send messages yet.");
378 int group = Integer.parseInt(args[1]);
379 byte cmd1 = (byte) HexUtils.toInteger(args[2]);
380 byte cmd2 = (byte) HexUtils.toInteger(args[3]);
381 Msg msg = Msg.makeBroadcastMessage(group, cmd1, cmd2);
382 getModem().writeMessage(msg);
383 console.println("Broadcast message sent to group " + group + ".");
384 console.println(msg.toString());
385 } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
386 console.println("Error while trying to create message.");
387 } catch (IOException e) {
388 console.println("Failed to send message.");
393 private void sendDirectMessage(Console console, MessageType messageType, String[] args) {
394 if (!InsteonAddress.isValid(args[1])) {
395 console.println("Invalid device address argument: " + args[1]);
396 } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
397 console.println("Invalid hex argument(s).");
398 } else if (!getModem().getDB().isComplete()) {
399 console.println("Not ready to send messages yet.");
402 InsteonAddress address = new InsteonAddress(args[1]);
403 byte cmd1 = (byte) HexUtils.toInteger(args[2]);
404 byte cmd2 = (byte) HexUtils.toInteger(args[3]);
406 if (messageType == MessageType.STANDARD) {
407 msg = Msg.makeStandardMessage(address, cmd1, cmd2);
409 byte[] data = HexUtils.toByteArray(args, 4, args.length);
410 boolean setCRC = getInsteonEngine(args[1]).supportsChecksum();
411 if (messageType == MessageType.EXTENDED) {
412 msg = Msg.makeExtendedMessage(address, cmd1, cmd2, data, setCRC);
414 msg = Msg.makeExtendedMessageCRC2(address, cmd1, cmd2, data);
417 getModem().writeMessage(msg);
418 console.println("Direct message sent to device " + address + ".");
419 console.println(msg.toString());
420 } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
421 console.println("Error while trying to create message.");
422 } catch (IOException e) {
423 console.println("Failed to send message.");
428 private void sendX10Message(Console console, String[] args) {
429 if (!X10Address.isValid(args[1])) {
430 console.println("Invalid x10 address argument: " + args[1]);
431 } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
432 console.println("Invalid hex argument(s).");
433 } else if (!getModem().getDB().isComplete()) {
434 console.println("Not ready to send messages yet.");
437 X10Address address = new X10Address(args[1]);
438 byte cmd = (byte) HexUtils.toInteger(args[2]);
439 Msg maddr = Msg.makeX10AddressMessage(address);
440 getModem().writeMessage(maddr);
441 Msg mcmd = Msg.makeX10CommandMessage(cmd);
442 getModem().writeMessage(mcmd);
443 console.println("X10 message sent to device " + address + ".");
444 console.println(maddr.toString());
445 console.println(mcmd.toString());
446 } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
447 console.println("Error while trying to create message.");
448 } catch (IOException e) {
449 console.println("Failed to send message.");
454 private void sendIMMessage(Console console, String[] args) {
455 if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
456 console.println("Invalid hex argument(s).");
457 } else if (!getModem().getDB().isComplete()) {
458 console.println("Not ready to send messages yet.");
461 Msg msg = Msg.makeMessage(args[1]);
462 byte[] data = msg.getData();
463 int headerLength = msg.getHeaderLength();
464 for (int i = 0; i + 2 < args.length; i++) {
465 data[i + headerLength] = (byte) HexUtils.toInteger(args[i + 2]);
467 getModem().writeMessage(msg);
468 console.println("IM message sent to the modem.");
469 console.println(msg.toString());
470 } catch (ArrayIndexOutOfBoundsException e) {
471 console.println("Too many data bytes provided.");
472 } catch (InvalidMessageTypeException e) {
473 console.println("Error while trying to create message.");
474 } catch (IOException e) {
475 console.println("Failed to send message.");