/* * #%L * OW2 Chameleon - Fuchsia Framework * %% * Copyright (C) 2009 - 2014 OW2 Chameleon * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ /* Calimero - A library for KNX network access Copyright (C) 2006-2008 W. Kastner This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package tuwien.auto.calimero.tools; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import tuwien.auto.calimero.CloseEvent; import tuwien.auto.calimero.IndividualAddress; import tuwien.auto.calimero.Settings; import tuwien.auto.calimero.exception.KNXException; import tuwien.auto.calimero.exception.KNXFormatException; import tuwien.auto.calimero.exception.KNXIllegalArgumentException; import tuwien.auto.calimero.knxnetip.KNXnetIPConnection; import tuwien.auto.calimero.link.KNXNetworkLink; import tuwien.auto.calimero.link.KNXNetworkLinkFT12; import tuwien.auto.calimero.link.KNXNetworkLinkIP; import tuwien.auto.calimero.link.medium.KNXMediumSettings; import tuwien.auto.calimero.link.medium.PLSettings; import tuwien.auto.calimero.link.medium.RFSettings; import tuwien.auto.calimero.link.medium.TPSettings; import tuwien.auto.calimero.log.LogLevel; import tuwien.auto.calimero.log.LogManager; import tuwien.auto.calimero.log.LogStreamWriter; import tuwien.auto.calimero.mgmt.Description; import tuwien.auto.calimero.mgmt.KnIPDeviceMgmtAdapter; import tuwien.auto.calimero.mgmt.PropertyAdapter; import tuwien.auto.calimero.mgmt.PropertyAdapterListener; import tuwien.auto.calimero.mgmt.PropertyClient; import tuwien.auto.calimero.mgmt.RemotePropertyServiceAdapter; import tuwien.auto.calimero.mgmt.PropertyClient.Property; import tuwien.auto.calimero.mgmt.PropertyClient.PropertyKey; /** * A tool for Calimero showing features of the {@link PropertyClient} used for KNX * property access. * <p> * PropClient is a console based tool implementation for reading and writing KNX * properties. It supports network access using a KNXnet/IP connection or FT1.2 * connection. To start the PropClient, invoke the <code>main</code>-method of this * class. Take a look at the command line options to configure the tool with the desired * communication settings. * <p> * The main part of this tool implementation interacts with the PropertyClient interface, * which offers high level access to KNX property information. It also shows creation of * the {@link PropertyAdapter}, necessary for a property client to work. All queried * property values, as well as occurring problems are written to <code>System.out * </code>. * * @author B. Malinowsky */ public class PropClient { private static final String tool = "PropClient"; private static final String version = "0.1"; private static final String sep = System.getProperty("line.separator"); private PropertyClient pc; private KNXNetworkLink lnk; private Map definitions; /** * Empty constructor. * <p> */ protected PropClient() {} /** * Entry point for running the PropClient. * <p> * An IP host or port identifier has to be supplied to specify the endpoint for the * KNX network access.<br> * To show the usage message of this tool on the console, supply the command line * option -help (or -h).<br> * Command line options are treated case sensitive. Available options for the property * client: * <ul> * <li><code>-help -h</code> show help message</li> * <li><code>-version</code> show tool/library version and exit</li> * <li><code>-verbose -v</code> enable verbose status output</li> * <li><code>-local -l</code> local device management</li> * <li><code>-remote -r</code> <i>KNX addr</i>  remote property service</li> * <li><code>-definitions -d</code> <i>file</i>  use property definition file</li> * <li><code>-localhost</code> <i>id</i>  local IP/host name</li> * <li><code>-localport</code> <i>number</i>  local UDP port (default system * assigned)</li> * <li><code>-port -p</code> <i>number</i>  UDP port on host (default 3671)</li> * <li><code>-nat -n</code> enable Network Address Translation</li> * <li><code>-serial -s</code> use FT1.2 serial communication</li> * </ul> * For local device management these options are available: * <ul> * <li><code>-emulatewriteenable -e</code> check write-enable of a property</li> * </ul> * For remote property service these options are available: * <ul> * <li><code>-routing</code> use KNXnet/IP routing</li> * <li><code>-medium -m</code> <i>id</i>  KNX medium [tp0|tp1|p110|p132|rf] * (defaults to tp1)</li> * <li><code>-connect -c</code> connection oriented mode</li> * <li><code>-authorize -a</code> <i>key</i>  authorize key to access KNX * device</li> * </ul> * * @param args command line options for property client */ public static void main(String[] args) { try { // read the command line options and run the client final Map options = new HashMap(); if (parseOptions(args, options)) new PropClient().run(options); } catch (final Throwable t) { if (t.getMessage() != null) System.out.println(t.getMessage()); } } private void run(Map options) throws KNXException, IOException { // create the supported user commands for KNX property access final Map commands = new HashMap(); commands.put("get", new GetProperty()); commands.put("set", new SetProperty()); commands.put("scan", new ScanProperties()); commands.put("desc", new GetDescription()); commands.put("?", new Help()); commands.put("quit", new Quit()); try { // create a property adapter and supply it to a new client pc = new PropertyClient(create(options)); // check if user supplied a XML resource with property definitions if (options.containsKey("defs")) PropertyClient.loadDefinitions((String) options.get("defs")); definitions = PropertyClient.getDefinitions(); // show some command info ((Command) commands.get("?")).execute(null); // create reader for user input final BufferedReader r = new BufferedReader(new InputStreamReader(System.in)); String[] args; while ((args = readLine(r)) != null) { if (args.length > 0) { final Command c = (Command) commands.get(args[0]); if (c == null) System.out.println("unknown command, type ? for help"); else if (args.length > 1 && args[1].equals("?")) c.printHelp(); else { // execute the requested command try { if (!c.execute(args)) break; } catch (final KNXException e) { if (!pc.isOpen()) throw e; System.out.println(e.getMessage()); } catch (final KNXIllegalArgumentException e) { System.out.println(e.getMessage()); } catch (final NumberFormatException e) { System.out.println("invalid number (" + e.getMessage() + ")"); } } } } } finally { if (pc != null) pc.close(); if (lnk != null) lnk.close(); } } /** * Creates the property adapter to be used with the property client depending on the * supplied user <code>options</code>. * <p> * There are two types of property adapters. One uses KNXnet/IP local device * management to access KNX properties in an interface object, the other type uses * remote property services. The remote adapter needs a KNX network link to access the * KNX network, the link is also created by this method if this adapter type is * requested. * * @param options contains parameters for property adapter creation * @return the created adapter * @throws KNXException on adapter creation problem */ private PropertyAdapter create(Map options) throws KNXException { // add a log writer for the console (System.out), setting a // user log level, if specified LogManager.getManager().addWriter(null, new ConsoleWriter(options.containsKey("verbose"))); // create local and remote socket address for use in adapter final InetSocketAddress local = createLocalSocket((InetAddress) options.get("localhost"), (Integer) options .get("localport")); final InetSocketAddress host = new InetSocketAddress((InetAddress) options.get("host"), ((Integer) options .get("port")).intValue()); // decide what type of adapter to create if (options.containsKey("localDM")) return createLocalDMAdapter(local, host, options); return createRemoteAdapter(local, host, options); } /** * Creates a local device management adapter. * <p> * * @param local local socket address * @param host remote socket address of host * @param options contains parameters for property adapter creation * @return local DM adapter * @throws KNXException on adapter creation problem */ private PropertyAdapter createLocalDMAdapter(InetSocketAddress local, InetSocketAddress host, Map options) throws KNXException { return new KnIPDeviceMgmtAdapter(local, host, options.containsKey("nat"), new PropertyListener(), options.containsKey("emulatewrite")); } /** * Creates a remote property service adapter for one device in the KNX network. * <p> * The adapter uses a KNX network link for access, also is created by this method. * * @param local local socket address * @param host remote socket address of host * @param options contains parameters for property adapter creation * @return remote property service adapter * @throws KNXException on adapter creation problem */ private PropertyAdapter createRemoteAdapter(InetSocketAddress local, InetSocketAddress host, Map options) throws KNXException { final KNXMediumSettings medium = (KNXMediumSettings) options.get("medium"); if (options.containsKey("serial")) { // create FT1.2 network link final String p = (String) options.get("serial"); try { lnk = new KNXNetworkLinkFT12(Integer.parseInt(p), medium); } catch (final NumberFormatException e) { lnk = new KNXNetworkLinkFT12(p, medium); } } else { final int mode = options.containsKey("routing") ? KNXNetworkLinkIP.ROUTER : KNXNetworkLinkIP.TUNNEL; lnk = new KNXNetworkLinkIP(mode, local, host, options.containsKey("nat"), medium); } final IndividualAddress remote = (IndividualAddress) options.get("remote"); final PropertyListener l = new PropertyListener(); // if an authorization key was supplied, the adapter uses // connection oriented mode and tries to authenticate final byte[] authKey = (byte[]) options.get("authorize"); if (authKey != null) return new RemotePropertyServiceAdapter(lnk, remote, l, authKey); return new RemotePropertyServiceAdapter(lnk, remote, l, options.containsKey("connect")); } /** * Writes command prompt and waits for command request from user. * <p> * * @param r input reader * @return array with command and command arguments * @throws IOException on I/O error */ private String[] readLine(BufferedReader r) throws IOException { System.out.print("> "); final String line = r.readLine(); return line != null ? split(line) : null; } private static boolean parseOptions(String[] args, Map options) throws KNXFormatException { if (args.length == 0) { System.out.println("A tool for KNX property access"); showVersion(); System.out.println("type -help for help message"); return false; } // add defaults options.put("port", new Integer(KNXnetIPConnection.IP_PORT)); options.put("medium", TPSettings.TP1); int i = 0; for (; i < args.length; i++) { final String arg = args[i]; if (isOption(arg, "-help", "-h")) { showUsage(); return false; } if (isOption(arg, "-version", null)) { showVersion(); return false; } if (isOption(arg, "-local", "-l")) options.put("localDM", null); else if (isOption(arg, "-remote", "-r")) options.put("remote", new IndividualAddress(args[++i])); else if (isOption(arg, "-definitions", "-d")) options.put("defs", args[++i]); else if (isOption(arg, "-verbose", "-v")) options.put("verbose", null); else if (isOption(arg, "-localhost", null)) parseHost(args[++i], true, options); else if (isOption(arg, "-localport", null)) options.put("localport", Integer.decode(args[++i])); else if (isOption(arg, "-port", "-p")) options.put("port", Integer.decode(args[++i])); else if (isOption(arg, "-nat", "-n")) options.put("nat", null); else if (isOption(arg, "-serial", "-s")) options.put("serial", null); else if (isOption(arg, "-medium", "-m")) options.put("medium", getMedium(args[++i])); else if (isOption(arg, "-emulatewriteenable", "-e")) options.put("emulatewrite", null); else if (isOption(arg, "-connect", "-c")) options.put("connect", null); else if (isOption(arg, "-authorize", "-a")) options.put("authorize", getAuthorizeKey(args[++i])); else if (isOption(arg, "-routing", null)) options.put("routing", null); else if (options.containsKey("serial")) // add port number/identifier to serial option options.put("serial", arg); else if (!options.containsKey("host")) parseHost(arg, false, options); else throw new IllegalArgumentException("unknown option " + arg); } if (!options.containsKey("localDM") && !options.containsKey("remote")) throw new IllegalArgumentException("no connection category specified"); if (!options.containsKey("host") && !options.containsKey("serial")) throw new IllegalArgumentException("no host or serial port specified"); if (options.containsKey("serial") && !options.containsKey("remote")) throw new IllegalArgumentException("-remote option is mandatory with -serial"); return true; } private static void showUsage() { final StringBuffer sb = new StringBuffer(); sb.append("usage: ").append(tool).append(" [options] <host|port>").append(sep); sb.append("options:").append(sep); sb.append(" -help -h show this help message").append(sep); sb.append(" -version show tool/library version and exit").append( sep); sb.append(" -verbose -v enable verbose status output").append(sep); sb.append(" -local -l local device management").append(sep); sb.append(" -remote -r <KNX addr> remote property service").append(sep); sb.append(" -definitions -d <file> use property definition file").append(sep); sb.append(" -localhost <id> local IP/host name").append(sep); sb.append(" -localport <number> local UDP port (default system assigned)") .append(sep); sb.append(" -port -p <number> UDP port on <host> (default ").append( KNXnetIPConnection.IP_PORT).append(")").append(sep); sb.append(" -nat -n enable Network Address Translation").append( sep); sb.append(" -serial -s use FT1.2 serial communication").append(sep); sb.append(" local DM only:").append(sep); sb.append(" -emulatewriteenable -e check write-enable of a property").append( sep); sb.append(" remote property service only:").append(sep); sb.append(" -routing use KNXnet/IP routing").append(sep); sb.append(" -medium -m <id> KNX medium [tp0|tp1|p110|p132|rf] " + "(default tp1)").append(sep); sb.append(" -connect -c connection oriented mode").append(sep); sb.append(" -authorize -a <key> authorize key to access KNX device").append( sep); System.out.println(sb); } // // utility methods // private static void showVersion() { System.out.println(tool + " version " + version + " using " + Settings.getLibraryHeader(false)); } private static KNXMediumSettings getMedium(String id) { // for now, the local device address is always left 0 in the // created medium setting, since there is no user cmd line option for this // so KNXnet/IP server will supply address if (id.equals("tp0")) return TPSettings.TP0; else if (id.equals("tp1")) return TPSettings.TP1; else if (id.equals("p110")) return new PLSettings(false); else if (id.equals("p132")) return new PLSettings(true); else if (id.equals("rf")) return new RFSettings(null); else throw new KNXIllegalArgumentException("unknown medium"); } private static byte[] getAuthorizeKey(String key) { final long value = Long.decode(key).longValue(); if (value < 0 || value > 0xFFFFFFFFL) throw new KNXIllegalArgumentException("invalid authorize key"); return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value }; } private static void parseHost(String host, boolean local, Map options) { try { options.put(local ? "localhost" : "host", InetAddress.getByName(host)); } catch (final UnknownHostException e) { throw new IllegalArgumentException("failed to read host " + host); } } private static InetSocketAddress createLocalSocket(InetAddress host, Integer port) { final int p = port != null ? port.intValue() : 0; try { return host != null ? new InetSocketAddress(host, p) : p != 0 ? new InetSocketAddress(InetAddress.getLocalHost(), p) : null; } catch (final UnknownHostException e) { throw new IllegalArgumentException("failed to create local host " + e.getMessage()); } } private static boolean isOption(String arg, String longOpt, String shortOpt) { return arg.equals(longOpt) || shortOpt != null && arg.equals(shortOpt); } private static void printHex(byte[] data, int elements) { final StringBuffer sb = new StringBuffer(); sb.append("hex: ["); for (int i = 0; i < data.length; ++i) { final int no = data[i] & 0xff; if (no < 0x10) sb.append('0'); sb.append(Integer.toHexString(no)); if (i > 0 && i % (data.length / elements) == 0) sb.append(", "); } sb.append("]"); System.out.println(sb); } private static String[] split(String text) { final StringTokenizer st = new StringTokenizer(text, " \t"); final String[] tokens = new String[st.countTokens()]; for (int i = 0; i < tokens.length; ++i) tokens[i] = st.nextToken(); return tokens; } private void printDescription(Description d) { final StringBuffer buf = new StringBuffer(); buf.append(d.getPropIndex()); buf.append(" OT " + d.getObjectType()); buf.append(", OI " + d.getObjectIndex()); buf.append(", PID " + d.getPID()); if (definitions != null) { Property p; if ((p = (Property) definitions.get(new PropertyClient.PropertyKey(d .getObjectType(), d.getPID()))) != null) buf.append(" (" + p.getName() + ")"); else if ((p = (Property) definitions.get(new PropertyClient.PropertyKey( PropertyKey.GLOBAL_OBJTYPE, d.getPID()))) != null) buf.append(" (" + p.getName() + ")"); } buf.append(", PDT " + (d.getPDT() == -1 ? "-" : Integer.toString(d.getPDT()))); buf.append(", curr. elems " + d.getCurrentElements()); buf.append(", max. " + d.getMaxElements()); buf.append(", r/w access " + d.getReadLevel() + "/" + d.getWriteLevel()); buf.append(d.isWriteEnabled() ? ", w.enabled" : ", r.only"); System.out.println(buf); } private static final class PropertyListener implements PropertyAdapterListener { PropertyListener() {} public void adapterClosed(CloseEvent e) { System.out.println(tool + " quits, " + e.getReason()); if (!e.isUserRequest()) System.exit(1); } } private abstract static class Command { String help; abstract boolean execute(String[] args) throws KNXException; final void printHelp() { System.out.println(help); } int toInt(String number) { return Integer.decode(number).intValue(); } } private final class GetProperty extends Command { GetProperty() { help = "get object-idx pid [start-idx elements]"; } boolean execute(String[] args) throws KNXException { if (args.length < 3 || args.length > 5) { System.out.println("sorry, wrong number of arguments"); return true; } final int oi = toInt(args[1]); final int pid = toInt(args[2]); try { if (args.length == 3) System.out.println(pc.getProperty(oi, pid)); } catch (final KNXException e) { printHex(pc.getProperty(oi, pid, 1, 1), 1); } try { if (args.length == 5) System.out.println(Arrays.asList( pc.getPropertyTranslated(oi, pid, toInt(args[3]), toInt(args[4])) .getAllValues()).toString()); } catch (final KNXException e) { printHex(pc.getProperty(oi, pid, toInt(args[3]), toInt(args[4])), toInt(args[4])); } return true; } } private final class GetDescription extends Command { GetDescription() { help = "desc object-idx pid" + sep + "desc object-idx \"i\" prop-idx"; } boolean execute(String[] args) throws KNXException { if (args.length == 3) printDescription(pc.getDescription(toInt(args[1]), toInt(args[2]))); else if (args.length == 4 && args[2].equals("i")) printDescription(pc.getDescriptionByIndex(toInt(args[1]), toInt(args[3]))); else System.out.println("sorry, wrong number of arguments"); return true; } } private final class SetProperty extends Command { SetProperty() { help = "set object-idx pid [start-idx] string-value" + sep + "set object-idx pid start-idx elements [\"0x\"|\"0\"|\"b\"]data" + sep + "(use hexadecimal format for more than 8 byte data or leading zeros)"; } boolean execute(String[] args) throws KNXException { if (args.length < 4 || args.length > 6) { System.out.println("sorry, wrong number of arguments"); return true; } final int cnt = args.length; final int oi = toInt(args[1]); final int pid = toInt(args[2]); if (cnt == 4) pc.setProperty(oi, pid, 1, args[3]); else if (cnt == 5) pc.setProperty(oi, pid, toInt(args[3]), args[4]); else if (cnt == 6) pc.setProperty(oi, pid, toInt(args[3]), toInt(args[4]), toByteArray(args[5])); return true; } private byte[] toByteArray(String s) { // use of BigXXX equivalent is a bit awkward, for now this is sufficient.. long l = 0; if (s.startsWith("0x") || s.startsWith("0X")) { final byte[] d = new byte[(s.length() - 1) / 2]; int k = (s.length() & 0x01) != 0 ? 3 : 4; for (int i = 2; i < s.length(); i = k, k += 2) d[(i - 1) / 2] = (byte) Short.parseShort(s.substring(i, k), 16); return d; } else if (s.length() > 1 && s.startsWith("0")) l = Long.parseLong(s, 8); else if (s.startsWith("b")) l = Long.parseLong(s.substring(1), 2); else l = Long.parseLong(s); int i = 0; for (long test = l; test != 0; test /= 0x100) ++i; final byte[] d = new byte[i == 0 ? 1 : i]; for (; i-- > 0; l /= 0x100) d[i] = (byte) (l & 0xff); return d; } } private final class ScanProperties extends Command { ScanProperties() { help = "scan [object-idx] [\"all\" for all object properties]"; } boolean execute(String[] args) throws KNXException { final int cnt = args.length; List l = Collections.EMPTY_LIST; if (cnt == 1) l = pc.scanProperties(false); else if (cnt == 2) { if (args[1].equals("all")) l = pc.scanProperties(true); else l = pc.scanProperties(toInt(args[1]), false); } else if (cnt == 3 && args[2].equals("all")) l = pc.scanProperties(toInt(args[1]), true); else System.out.println("sorry, wrong number of arguments"); for (final Iterator i = l.iterator(); i.hasNext();) { final Description d = (Description) i.next(); printDescription(d); } return true; } } private final class Help extends Command { Help() { help = "show command help"; } boolean execute(String[] args) { final StringBuffer buf = new StringBuffer(); buf.append("commands: get | set | desc | scan | quit (append ? for help)" + sep); buf.append("get - read property value(s)" + sep); buf.append("set - write property value(s)" + sep); buf.append("desc - read one property description" + sep); buf.append("scan - read property descriptions" + sep); buf.append("quit - quit this program" + sep); System.out.print(buf); return true; } } private final class Quit extends Command { Quit() { help = "close property client and quit"; } boolean execute(String[] args) { return false; } } private static final class ConsoleWriter extends LogStreamWriter { ConsoleWriter(boolean verbose) { super(verbose ? LogLevel.INFO : LogLevel.ERROR, System.out, true); } public void close() {} } }