]> git.basschouten.com Git - openhab-addons.git/blob
d35e495b61c3048d487009ecb44b499eee1bb2b5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.insteon.internal.command;
14
15 import java.io.File;
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;
25 import java.util.Set;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
28
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;
46
47 /**
48  *
49  * The {@link DebugCommand} represents an Insteon console debug command
50  *
51  * @author Jeremy Setton - Initial contribution
52  */
53 @NonNullByDefault
54 public class DebugCommand extends InsteonCommand implements PortListener {
55     private static final String NAME = "debug";
56     private static final String DESCRIPTION = "Insteon debug commands";
57
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";
67
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);
71
72     private static final String ALL_OPTION = "--all";
73
74     private static final String MSG_EVENTS_FILE_PREFIX = "messageEvents";
75
76     private static enum MessageType {
77         STANDARD,
78         EXTENDED,
79         EXTENDED_2
80     }
81
82     private final Logger logger = LoggerFactory.getLogger(DebugCommand.class);
83
84     private boolean monitoring = false;
85     private boolean monitorAllDevices = false;
86     private Set<InsteonAddress> monitoredAddresses = new HashSet<>();
87
88     public DebugCommand(InsteonCommandExtension commandExtension) {
89         super(NAME, DESCRIPTION, commandExtension);
90     }
91
92     @Override
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"));
110     }
111
112     @Override
113     public void execute(String[] args, Console console) {
114         if (args.length == 0) {
115             printUsage(console);
116             return;
117         }
118
119         switch (args[0]) {
120             case LIST_MONITORED:
121                 if (args.length == 1) {
122                     listMonitoredDevices(console);
123                 } else {
124                     printUsage(console, args[0]);
125                 }
126                 break;
127             case START_MONITORING:
128                 if (args.length == 2) {
129                     startMonitoring(console, args[1]);
130                 } else {
131                     printUsage(console, args[0]);
132                 }
133                 break;
134             case STOP_MONITORING:
135                 if (args.length == 2) {
136                     stopMonitoring(console, args[1]);
137                 } else {
138                     printUsage(console, args[0]);
139                 }
140                 break;
141             case SEND_BROADCAST_MESSAGE:
142                 if (args.length == 4) {
143                     sendBroadcastMessage(console, args);
144                 } else {
145                     printUsage(console, args[0]);
146                 }
147                 break;
148             case SEND_STANDARD_MESSAGE:
149                 if (args.length == 4) {
150                     sendDirectMessage(console, MessageType.STANDARD, args);
151                 } else {
152                     printUsage(console, args[0]);
153                 }
154                 break;
155             case SEND_EXTENDED_MESSAGE:
156                 if (args.length >= 4 && args.length <= 17) {
157                     sendDirectMessage(console, MessageType.EXTENDED, args);
158                 } else {
159                     printUsage(console, args[0]);
160                 }
161                 break;
162             case SEND_EXTENDED_2_MESSAGE:
163                 if (args.length >= 4 && args.length <= 16) {
164                     sendDirectMessage(console, MessageType.EXTENDED_2, args);
165                 } else {
166                     printUsage(console, args[0]);
167                 }
168                 break;
169             case SEND_X10_MESSAGE:
170                 if (args.length == 3) {
171                     sendX10Message(console, args);
172                 } else {
173                     printUsage(console, args[0]);
174                 }
175                 break;
176             case SEND_IM_MESSAGE:
177                 if (args.length >= 2) {
178                     sendIMMessage(console, args);
179                 } else {
180                     printUsage(console, args[0]);
181                 }
182                 break;
183             default:
184                 console.println("Unknown command '" + args[0] + "'");
185                 printUsage(console);
186                 break;
187         }
188     }
189
190     @Override
191     public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
192         List<String> strings = List.of();
193         if (cursorArgumentIndex == 0) {
194             strings = SUBCMDS;
195         } else if (cursorArgumentIndex == 1) {
196             switch (args[0]) {
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();
201                     break;
202                 case SEND_BROADCAST_MESSAGE:
203                     strings = getModem().getDB().getBroadcastGroups().stream().map(String::valueOf).toList();
204                     break;
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();
209                     break;
210                 case SEND_X10_MESSAGE:
211                     strings = getModem().getX10Devices().stream().map(X10Device::getAddress).map(X10Address::toString)
212                             .toList();
213                     break;
214                 case SEND_IM_MESSAGE:
215                     strings = MsgDefinitionRegistry.getInstance().getDefinitions().entrySet().stream()
216                             .filter(entry -> entry.getValue().getDirection() == Direction.TO_MODEM).map(Entry::getKey)
217                             .toList();
218                     break;
219             }
220         }
221
222         return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
223     }
224
225     @Override
226     public void disconnected() {
227         // do nothing
228     }
229
230     @Override
231     public void messageReceived(Msg msg) {
232         try {
233             InsteonAddress address = msg.getInsteonAddress(msg.isReply() ? "toAddress" : "fromAddress");
234             if (monitorAllDevices || monitoredAddresses.contains(address)) {
235                 logMessageEvent(address, msg);
236             }
237         } catch (FieldException ignored) {
238             // ignore message with no address field
239         }
240     }
241
242     @Override
243     public void messageSent(Msg msg) {
244         try {
245             InsteonAddress address = msg.getInsteonAddress("toAddress");
246             if (monitorAllDevices || monitoredAddresses.contains(address)) {
247                 logMessageEvent(address, msg);
248             }
249         } catch (FieldException ignored) {
250             // ignore message with no address field
251         }
252     }
253
254     private String getMsgEventsFileName(String address) {
255         return MSG_EVENTS_FILE_PREFIX + "-" + address.replace(".", "") + ".log";
256     }
257
258     private String getMsgEventsFilePath(String address) {
259         return InsteonBindingConstants.BINDING_DATA_DIR + File.separator + getMsgEventsFileName(address);
260     }
261
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);
265
266         if (folder.isDirectory()) {
267             Arrays.asList(folder.listFiles()).stream().filter(file -> file.getName().startsWith(prefix))
268                     .forEach(File::delete);
269         }
270     }
271
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());
275
276         try {
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");
281             }
282             parent.mkdirs();
283             file.createNewFile();
284
285             PrintStream ps = new PrintStream(new FileOutputStream(file, true));
286             ps.println(timestamp + " " + msg.toString());
287             ps.close();
288         } catch (IOException e) {
289             logger.warn("failed to write to message event file", e);
290         }
291     }
292
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.");
299         } else {
300             console.println("Not monitoring any devices.");
301         }
302     }
303
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);
312             } else {
313                 console.println("Already monitoring all devices.");
314             }
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);
322             } else {
323                 console.println("Already monitoring the device " + address + ".");
324             }
325         } else {
326             console.println("Invalid device address" + address + ".");
327             return;
328         }
329
330         if (!monitoring) {
331             getModem().getPort().registerListener(this);
332             monitoring = true;
333         }
334     }
335
336     private void stopMonitoring(Console console, String address) {
337         if (!monitoring) {
338             console.println("Not monitoring any devices.");
339             return;
340         }
341
342         if (ALL_OPTION.equals(address)) {
343             if (monitorAllDevices) {
344                 monitorAllDevices = false;
345                 console.println("Stopped monitoring all devices.");
346             } else {
347                 console.println("Not monitoring all devices.");
348             }
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 + ".");
354             } else {
355                 console.println("Not monitoring the device " + address + ".");
356                 return;
357             }
358         } else {
359             console.println("Invalid address device address " + address + ".");
360             return;
361         }
362
363         if (!monitorAllDevices && monitoredAddresses.isEmpty()) {
364             getModem().getPort().unregisterListener(this);
365             monitoring = false;
366         }
367     }
368
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.");
376         } else {
377             try {
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.");
389             }
390         }
391     }
392
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.");
400         } else {
401             try {
402                 InsteonAddress address = new InsteonAddress(args[1]);
403                 byte cmd1 = (byte) HexUtils.toInteger(args[2]);
404                 byte cmd2 = (byte) HexUtils.toInteger(args[3]);
405                 Msg msg;
406                 if (messageType == MessageType.STANDARD) {
407                     msg = Msg.makeStandardMessage(address, cmd1, cmd2);
408                 } else {
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);
413                     } else {
414                         msg = Msg.makeExtendedMessageCRC2(address, cmd1, cmd2, data);
415                     }
416                 }
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.");
424             }
425         }
426     }
427
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.");
435         } else {
436             try {
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.");
450             }
451         }
452     }
453
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.");
459         } else {
460             try {
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]);
466                 }
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.");
476             }
477         }
478     }
479 }