diff --git a/README.md b/README.md index 279379e..f722c57 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # KNXmap -A tool for scanning and auditing KNXnet/IP gateways on IP driven networks. In addition to search and identify gateways KNXmap allows to scan for devices on the KNX bus via KNXnet/IP gateways. +A tool for scanning and auditing KNXnet/IP gateways on IP driven networks. KNXnet/IP defines Ethernet as physical communication media for KNX (EN 50090, ISO/IEC 14543). KNXmap also allows to scan for devices on the KNX bus via KNXnet/IP gateways. In addition to scanning, KNXmap supports other modes to interact with KNX gateways like monitor bus messages or write arbitrary values to group addresses. ## Compatibility -KNXmap is based on the [asyncio](https://docs.python.org/3/library/asyncio.html) module which is available for Python 3.3 and newer. Users of Python 3.3 must install `asyncio` from [PyPI](https://pypi.python.org/pypi), Python 3.4 ships it in the standard library by default. Therefore KNXmap requires Python 3.3 or any newer version of Python. +KNXmap is based on the [asyncio](https://docs.python.org/3/library/asyncio.html) module which is available for Python 3.3 and newer. Users of Python 3.3 must install `asyncio` from [PyPI](https://pypi.python.org/pypi), Python 3.4 ships it in the standard library. +<<<<<<< HEAD ## KNX KNX is a standardized (EN 50090, ISO/IEC 14543), OSI-based network communications protocol for building automation. KNX is the successor to, and convergence of, three previous standards: the European Home Systems Protocol (EHS), BatiBUS, and the European Installation Bus (EIB or Instabus). The KNX standard is administered by the [KNX Association](https://www.knx.org/knx-en/index.php). ([Source](https://en.wikipedia.org/wiki/KNX_\(standard\))) @@ -15,6 +16,23 @@ KNX is a standardized (EN 50090, ISO/IEC 14543), OSI-based network communication KNXnet/IP defines Ethernet as physical communication media. It basically allows administrators to manage KNX bus devices via IP driven networks. **Note**: Unfourtunately the standard is proprietary which makes it impossible to be included in this repository. +======= +## Usage + +Install and run KNXmap: + +``` +python setup.py install +knxmap.py --help +``` + +Or just invoke the script locally: + +``` +chmod +x knxmap.py +./knxmap.py --help +``` +>>>>>>> develop ## Scanning Modes @@ -29,26 +47,28 @@ KNXmap supports three different scanning modes: This is the default mode of KNXmap. It sends KNX description request to the supplied targets in order to chceck if they are KNXnet/IP gateways. ``` -knxmap.py 192.168.1.100 +knxmap.py scan 192.168.1.100 ``` KNXmap supports to scan multiple targets at once by supplying multiple IP addresses separated by a space. Targets can also be defined as networks in CIDR notation: ``` -knxmap.py 192.168.1.100 192.168.1.110 192.168.2.0/24 +knxmap.py scan 192.168.1.100 192.168.1.110 192.168.2.0/24 ``` -**Note**: Many KNXnet/IP gateways fail to properly handle subsequent discovery requests. As a consequence, discovering such devices can be quite unreliable! - ### Bus Mode In addition to the discovery mode, KNXmap also supports to scan for devices on the KNX bus. ``` -knxmap.py --bus-targets 1.0.0-1.1.255 192.168.1.100 +knxmap.py scan 192.168.1.100 --bus-targets 1.1.5 ``` -**Note**: Currently only target ranges are allowed, so at least two devices must be scanned because e.g. 1.1.1-1.1.1 is not a valid target definition. +KNXmap also supports bus address ranges: + +``` +knxmap.py scan 192.168.1.100 --bus-targets 1.0.0-1.1.255 +``` The default mode is to only check if sending messages to a address returns an error or not. This helps to identify potential devices and alive targets. @@ -57,7 +77,7 @@ The default mode is to only check if sending messages to a address returns an er In addition to the default bus scanning KNXmap can also extract basic information from devices for further identification by supplying the `--bus-info` argument: ``` -knxmap.py --bus-targets 1.0.0-1.1.255 --bus-info 192.168.1.100 +knxmap.py scan 192.168.1.100 --bus-targets 1.1.5 --bus-info ``` ### Search Mode @@ -65,7 +85,7 @@ knxmap.py --bus-targets 1.0.0-1.1.255 --bus-info 192.168.1.100 KNX supports finding devices by sending multicast packets that should be answered by any KNXnet/IP gateway. KNXmap supports gateway searching via the `--search` flag. It requires the `-i`/`--interface` and superuser privileges: ``` -sudo knxmap.py --search --interface eth1 +sudo knxmap.py --interface eth1 search ``` **Note**: Packet filtering rules might block the response packets. If there are no KNXnet/IP gateways answering their packets might be dropped by netfilter/iptables rules. @@ -74,9 +94,9 @@ sudo knxmap.py --search --interface eth1 KNXmap supports two different monitoring modes: -* Bus monitoring (`--bus-monitor`) prints the raw messages received from the KNX bus. -* Group monitoring (`--group-monitor`) prints all group messages received from the KNX bus. +* Bus monitoring: prints the raw messages received from the KNX bus. +<<<<<<< HEAD These monitoring modes can be useful for debugging communication on the bus. Additionally, they can be used for passive information gathering which allows to identify bus devices without sending messages to any individual or group address. Especially motion sensors or other devices that frequently send messages to the bus can easily be identified via bus monitoring. ## TODO @@ -84,11 +104,32 @@ These monitoring modes can be useful for debugging communication on the bus. Add * Implement KNXnet/IP Routing (bus.py) * KNXnet/IP router device required (not available yet) * Implement [KNX ObjectServer protocol](http://www.weinzierl.de/images/download/products/770/KNX_BAOS_Protocol.pdf) (objectserver.py) +======= +``` +knxmap.py monitor 192.168.1.100 +``` + +* Group monitoring: prints all group messages received from the KNX bus. + +``` +knxmap.py monitor 192.168.1.100 --group-monitor +``` + +These monitoring modes can be useful for debugging communication on the bus. Additionally, they can be used for passive information gathering which allows to identify bus devices without sending messages to any individual or group address. Especially motion sensors or other devices that frequently send messages to the bus can easily be identified via bus monitoring. + +## Group Write + +KNXmap allows one to write arbitrary values to any group address on the bus. The following example writes the value `1` to the group address `0/0/1`: + +``` +knxmap.py write 192.168.1.100 0/0/1 1 +``` +>>>>>>> develop ## Hacking Enable full debugging and verbosity for development: ``` -PYTHONASYNCIODEBUG=1 knxmap.py 192.168.178.20 --bus-targets 1.1.0-1.1.6 --bus-info -v -``` \ No newline at end of file +PYTHONASYNCIODEBUG=1 knxmap.py -v scan 192.168.178.20 --bus-targets 1.1.0-1.1.6 --bus-info +``` diff --git a/knxmap.py b/knxmap.py index 648eb05..cc02e6e 100755 --- a/knxmap.py +++ b/knxmap.py @@ -4,12 +4,12 @@ import argparse import logging -from libknx import KnxScanner, Targets, KnxTargets +from libknxmap import KnxMap, Targets, KnxTargets # asyncio requires at least Python 3.3 if sys.version_info.major < 3 or \ - (sys.version_info.major > 2 and - sys.version_info.minor < 3): + (sys.version_info.major > 2 and + sys.version_info.minor < 3): print('At least Python version 3.3 is required to run this script!') sys.exit(1) try: @@ -21,93 +21,134 @@ sys.exit(1) LOGGER = logging.getLogger(__name__) +ARGS = argparse.ArgumentParser( + description='KNXnet/IP network and bus mapper', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) +SUBARGS = ARGS.add_subparsers(dest='cmd') -# TODO: create proper arguments -# TODO: add subcommands for scanning modes? -# TODO: add dump-file argument for monitoring modes -# TODO: implement key bruteforcing for authorization request PINs -ARGS = argparse.ArgumentParser(description="KNXnet/IP Scanner") +# General options ARGS.add_argument( - 'targets', nargs='*', - default=[], help='Target hostnames/IP addresses') + '-v', '--verbose', action='count', dest='level', + default=2, help='Verbose logging (repeat for more verbose)') +ARGS.add_argument( + '-q', '--quiet', action='store_const', const=0, dest='level', + default=2, help='Only log errors') ARGS.add_argument( - '-p', '--port', action='store', dest='port', type=int, + '-p', action='store', dest='port', type=int, default=3671, help='UDP port to be scanned') +ARGS.add_argument( + '-i', action='store', dest='iface', + default=None, help='Interface to be used') ARGS.add_argument( '--workers', action='store', type=int, metavar='N', default=30, help='Limit concurrent workers') ARGS.add_argument( - '-i', '--interface', action='store', dest='iface', - default=None, help='Interface to be used') + '--key', action='store', dest='auth_key', type=int, + default=0xffffffff, help='Authorize key for System 2 and System 7 devices') ARGS.add_argument( - '--search', action='store_true', dest='search_mode', - default=False, help='Find local KNX gateways via search requests') -ARGS.add_argument( - '--search-timeout', action='store', dest='search_timeout', type=int, - default=5, help='Timeout in seconds for multicast responses') + '--timeout', action='store', dest='timeout', type=int, + default=2, help='Timeout in seconds for unicast description responses') ARGS.add_argument( + '--retries', action='store', dest='retries', type=int, + default=3, help='Count of retries for description requests') + +pscan = SUBARGS.add_parser('scan', help='Scan KNXnet/IP gateways and attached bus devices') +pscan.add_argument( + 'targets', help='KNXnet/IP gateway', metavar='gateway') +pscan.add_argument( '--bus-targets', action='store', dest='bus_targets', - default=None, help='Bus target range') -ARGS.add_argument( + default=None, help='Bus target range (e.g. 1.1.0-1.1.10)') +pscan.add_argument( '--bus-info', action='store_true', dest='bus_info', - default=False, help='Try to extract information from bus devices') -ARGS.add_argument( - '--bus-monitor', action='store_true', dest='bus_monitor_mode', - default=False, help='Monitor all bus messages via KNXnet/IP gateway') -ARGS.add_argument( + default=False, help='Try to extract information from alive bus devices') + +psearch = SUBARGS.add_parser('search', + help='Search for KNXnet/IP gateways on the local network') +psearch.add_argument( + '--search-timeout', action='store', dest='search_timeout', type=int, + default=5, help='Timeout in seconds for multicast responses') + +pwrite = SUBARGS.add_parser('write', help='Write a value to a group address') +pwrite.add_argument( + 'targets', help='KNXnet/IP gateway', metavar='gateway') +pwrite.add_argument( + 'group_write_address', help='A KNX group address to write to') +pwrite.add_argument( + 'group_write_value', default=0, help='Value to write to the group address') +pwrite.add_argument( + '--routing', action='store_true', dest='routing', + default=False, help='Use Routing instead of Tunnelling') + +pbrute = SUBARGS.add_parser('brute', help='Bruteforce authentication key') +pbrute.add_argument( + 'targets', help='KNXnet/IP gateway', metavar='gateway') +pbrute.add_argument( + 'bus_target', help='Individual address of bus device') + +pmonitor = SUBARGS.add_parser('monitor', help='Monitor bus and group messages') +pmonitor.add_argument( + 'targets', help='KNXnet/IP gateway', metavar='gateway') +pmonitor.add_argument( '--group-monitor', action='store_true', dest='group_monitor_mode', - default=False, help='Monitor group bus messages via KNXnet/IP gateway') -ARGS.add_argument( - '-v', '--verbose', action='count', dest='level', - default=2, help='Verbose logging (repeat for more verbose)') -ARGS.add_argument( - '-q', '--quiet', action='store_const', const=0, dest='level', - default=2, help='Only log errors') + default=False, help='Monitor group instead of messages via KNXnet/IP gateway') def main(): args = ARGS.parse_args() - if not args.targets and not args.search_mode: - ARGS.print_help() - sys.exit() - - targets = Targets(args.targets, args.port) - bus_targets = KnxTargets(args.bus_targets) levels = [logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] format = '[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s' if args.level > 2 else '%(message)s' - logging.basicConfig(level=levels[min(args.level, len(levels)-1)], format=format) + logging.basicConfig(level=levels[min(args.level, len(levels) - 1)], format=format) loop = asyncio.get_event_loop() - if args.search_mode: - if not args.iface: - LOGGER.error('--search option requires -i/--interface argument') - sys.exit(1) - - if os.geteuid() != 0: - LOGGER.error('-i/--interface option requires superuser privileges') - sys.exit(1) + if hasattr(args, 'targets'): + targets = Targets(args.targets, args.port) + knxmap = KnxMap(targets=targets.targets, max_workers=args.workers) else: - LOGGER.info('Scanning {} target(s)'.format(len(targets.targets))) - - scanner = KnxScanner(targets=targets.targets, max_workers=args.workers) + knxmap = KnxMap(max_workers=args.workers) try: - loop.run_until_complete(scanner.scan( - search_mode=args.search_mode, - search_timeout=args.search_timeout, - bus_targets=bus_targets.targets, - bus_info=args.bus_info, - bus_monitor_mode=args.bus_monitor_mode, - group_monitor_mode=args.group_monitor_mode, - iface=args.iface)) + if args.cmd == 'search': + if not args.iface: + LOGGER.error('--search option requires -i/--interface argument') + sys.exit(1) + if os.geteuid() != 0: + LOGGER.error('-i/--interface option requires superuser privileges') + sys.exit(1) + loop.run_until_complete(knxmap.search( + search_timeout=args.search_timeout, + iface=args.iface)) + elif args.cmd == 'write': + loop.run_until_complete(knxmap.group_writer( + target=args.group_write_address, + value=args.group_write_value, + routing=args.routing, + desc_timeout=args.timeout, + desc_retries=args.retries, + iface=args.iface)) + elif args.cmd == 'monitor': + loop.run_until_complete(knxmap.monitor( + group_monitor_mode=args.group_monitor_mode)) + elif args.cmd == 'brute': + loop.run_until_complete(knxmap.brute( + bus_target=KnxTargets(args.bus_target))) + elif args.cmd == 'scan': + LOGGER.info('Scanning {} target(s)'.format(len(targets.targets))) + bus_targets = KnxTargets(args.bus_targets) + loop.run_until_complete(knxmap.scan( + desc_timeout=args.timeout, + desc_retries=args.retries, + bus_targets=bus_targets.targets, + bus_info=args.bus_info, + auth_key=args.auth_key)) except KeyboardInterrupt: for t in asyncio.Task.all_tasks(): t.cancel() loop.run_forever() - if scanner.bus_protocols: - # Make sure to send a DISCONNECT_REQUEST when the bus monitor will be closed - for p in scanner.bus_protocols: + if knxmap.bus_protocols: + # Make sure to send a DISCONNECT_REQUEST + # when the bus monitor will be closed. + for p in knxmap.bus_protocols: p.knx_tunnel_disconnect() finally: loop.close() diff --git a/libknx/bus.py b/libknx/bus.py deleted file mode 100644 index 566109d..0000000 --- a/libknx/bus.py +++ /dev/null @@ -1,388 +0,0 @@ -"""This module will include code that scans on the KNX bus for -available devices.""" -import logging -import asyncio - -from .messages import * -from .core import * - -LOGGER = logging.getLogger(__name__) - - -class KnxTunnelConnection(asyncio.DatagramProtocol): - """Communicate with bus devices via a KNX gateway using TunnellingRequests. A tunneling - connection is always used if the bus destination is a physical KNX address.""" - def __init__(self, future, connection_type=0x04, layer_type='TUNNEL_LINKLAYER', loop=None): - self.future = future - self.connection_type = connection_type - self.layer_type = layer_type - self.target_futures = dict() - self.loop = loop or asyncio.get_event_loop() - self.transport = None - self.tunnel_established = False - self.communication_channel = None - self.sequence_count = 0 # sequence counter in KNX body - self.tpci_sequence_count = 0 # NCD/NPD counter - self.knx_source_address = None # TODO: probably not needed - self.response_queue = list() - - def connection_made(self, transport): - self.transport = transport - self.peername = self.transport.get_extra_info('peername') - self.sockname = self.transport.get_extra_info('sockname') - connect_request = KnxConnectRequest( - sockname=self.sockname, - connection_type=self.connection_type, - layer_type=self.layer_type) - self.transport.sendto(connect_request.get_message()) - # Schedule CONNECTIONSTATE_REQUEST to keep the connection alive - self.loop.call_later(50, self.knx_keep_alive) - self.loop.call_later(4, self.poll_response_queue) - - def poll_response_queue(self): - if self.response_queue: - for response in self.response_queue: - knx_source = response.parse_knx_address(response.body.get('cemi').get('knx_source')) - knx_dest = response.parse_knx_address(response.body.get('cemi').get('knx_destination')) - if not knx_source and not knx_dest: - continue - - if knx_dest in self.target_futures.keys(): - if not self.target_futures[knx_dest].done(): - self.target_futures[knx_dest].set_result(response) - del self.target_futures[knx_dest] - elif knx_source in self.target_futures.keys(): - if not self.target_futures[knx_source].done(): - self.target_futures[knx_source].set_result(response) - del self.target_futures[knx_source] - - self.loop.call_later(2, self.poll_response_queue) - - def response_timeout(self, target): - if target in self.target_futures.keys(): - if not self.target_futures[target].done(): - self.target_futures[target].set_result(False) - del self.target_futures[target] - - def datagram_received(self, data, addr): - LOGGER.debug('data: {}'.format(data)) - knx_message = parse_message(data) - - if not knx_message: - LOGGER.error('Invalid KNX message: {}'.format(data)) - self.knx_tunnel_disconnect() - self.transport.close() - self.future.set_result(None) - return - - if isinstance(knx_message, KnxConnectResponse): - if not knx_message.ERROR: - if not self.tunnel_established: - self.tunnel_established = True - self.communication_channel = knx_message.body.get('communication_channel_id') - self.knx_source_address = knx_message.body.get('data_block').get('knx_address') - self.future.set_result(True) - else: - LOGGER.error(knx_message.ERROR) - self.transport.close() - self.future.set_result(None) - elif isinstance(knx_message, KnxTunnellingRequest): - knx_source = knx_message.parse_knx_address(knx_message.body.get('cemi').get('knx_source')) - knx_dest = knx_message.parse_knx_address(knx_message.body.get('cemi').get('knx_destination')) - - if CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.con' and \ - (knx_message.body.get('cemi').get('tpci').get('type') == TPCI_TYPES['UCD'] or - knx_message.body.get('cemi').get('tpci').get('type') == TPCI_TYPES['NCD']): - - if knx_message.body.get('cemi').get('controlfield_1').get('confirm'): - LOGGER.debug('KNX device not alive: {}'.format(knx_dest)) - if knx_dest in self.target_futures.keys(): - if not self.target_futures[knx_dest].done(): - self.target_futures[knx_dest].set_result(False) - del self.target_futures[knx_dest] - else: - self.response_queue.append(knx_message) - - else: - LOGGER.debug('KNX device is alive: {}'.format(knx_dest)) - if knx_dest in self.target_futures.keys(): - if not self.target_futures[knx_dest].done(): - self.target_futures[knx_dest].set_result(True) - del self.target_futures[knx_dest] - else: - self.response_queue.append(knx_message) - - elif CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.con' and \ - knx_message.body.get('cemi').get('tpci').get('type') == TPCI_TYPES['NDP']: - - # if we get a confirmation for our device descriptor request, check if L_Data.ind arrives - if knx_message.body.get('cemi').get('apci') == APCI_TYPES.get('A_DeviceDescriptor_Read'): - self.loop.call_later(3, self.response_timeout, knx_dest) - - elif CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.ind' and \ - knx_message.body.get('cemi').get('tpci').get('type') == TPCI_TYPES['UCD']: - - if knx_message.body.get('cemi').get('tpci').get('status') is 1: - if knx_dest in self.target_futures.keys(): - if not self.target_futures[knx_dest].done(): - self.target_futures[knx_dest].set_result(False) - del self.target_futures[knx_dest] - else: - self.response_queue.append(knx_message) - - elif CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.ind' and \ - knx_message.body.get('cemi').get('tpci').get('type') == TPCI_TYPES['NDP']: - - if knx_message.body.get('cemi').get('apci') == APCI_TYPES['A_DeviceDescriptor_Response']: - LOGGER.debug( - '{}: DEVICEDESCRIPTOR_RESPONSE DATA: {}'.format(knx_source, knx_message.body.get('cemi').get('data'))) - - elif knx_message.body.get('cemi').get('apci') == APCI_TYPES['A_Authorize_Response']: - LOGGER.debug( - '{}: AUTHORIZE_RESPONSE DATA: {}'.format(knx_source, knx_message.body.get('cemi').get('data'))) - - elif knx_message.body.get('cemi').get('apci') == APCI_TYPES['A_PropertyValue_Response']: - LOGGER.debug('{}/{}/{}: PROPERTY_VALUE_RESPONSE DATA: {}'.format( - self.peername[0], knx_source, knx_dest, knx_message.body.get('cemi').get('data')[4:])) - - elif knx_message.body.get('cemi').get('apci') == APCI_TYPES['A_Memory_Response']: - LOGGER.debug('{}/{}: MEMORY_RESPONSE DATA: {}'.format( - self.peername[0], knx_source, knx_message.body.get('cemi').get('data'))) - - - # If we receive any Numbered Data Packets for - # targets without futures, add the knx_source - # to the response queue for later processing. - if knx_source in self.target_futures.keys(): - if not self.target_futures[knx_source].done(): - self.target_futures[knx_source].set_result(knx_message) - del self.target_futures[knx_source] - else: - self.response_queue.append(knx_message) - - # If we receive any L_Data.con or L_Data.ind from a KNXnet/IP gateway - # we have to reply with a tunnelling ack. - if CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.con' or \ - CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.ind': - tunnelling_ack = KnxTunnellingAck( - communication_channel=knx_message.body.get('communication_channel_id'), - sequence_count=knx_message.body.get('sequence_counter')) - self.transport.sendto(tunnelling_ack.get_message()) - - elif isinstance(knx_message, KnxTunnellingAck): - pass - elif isinstance(knx_message, KnxDeviceConfigurationRequest): - conf_ack = KnxDeviceConfigurationAck( - communication_channel=knx_message.body.get('communication_channel_id'), - sequence_count=knx_message.body.get('sequence_counter')) - self.transport.sendto(conf_ack.get_message()) - elif isinstance(knx_message, KnxDeviceConfigurationAck): - pass - elif isinstance(knx_message, KnxConnectionStateResponse): - # After receiving a CONNECTIONSTATE_RESPONSE shedule the next one - self.loop.call_later(50, self.knx_keep_alive) - elif isinstance(knx_message, KnxDisconnectRequest): - disconnect_response = KnxDisconnectResponse(communication_channel=self.communication_channel) - self.transport.sendto(disconnect_response.get_message()) - self.transport.close() - if not self.future.done(): - self.future.set_result(None) - elif isinstance(knx_message, KnxDisconnectResponse): - self.transport.close() - if not self.future.done(): - self.future.set_result(None) - - def send_data(self, data, target=None): - """A wrapper for sendto() that takes care of incrementing the sequence counter. - - Note: the sequence counter field is only 1 byte. After incrementing the counter - to 255, it seems to be OK to just start over from 0. At least this applies - to the tested devices.""" - f = asyncio.Future() - if target: - self.target_futures[target] = f - self.transport.sendto(data) - if self.sequence_count == 255: - self.sequence_count = 0 - else: - self.sequence_count += 1 - return f - - def tpci_connect(self, target): - tunnel_request = self.make_tunnel_request(target) - tunnel_request.tpci_unnumbered_control_data('CONNECT') - return self.send_data(tunnel_request.get_message(), target) - - def tpci_disconnect(self, target): - tunnel_request = self.make_tunnel_request(target) - tunnel_request.tpci_unnumbered_control_data('DISCONNECT') - return self.send_data(tunnel_request.get_message(), target) - - def tpci_send_ncd(self, target): - tunnel_request = self.make_tunnel_request(target) - tunnel_request.tpci_numbered_control_data('ACK', sequence=self.tpci_sequence_count) - # increment TPCI sequence counter - if self.tpci_sequence_count == 15: - self.tpci_sequence_count = 0 - else: - self.tpci_sequence_count += 1 - return self.send_data(tunnel_request.get_message(), target) - - def make_tunnel_request(self, knx_destination): - """A helper function that returns a KnxTunnellingRequest that is already predefined - for the current tunnel connection. It already sets the communication channel, the - sequence count, the KNX source address and the peer which are all handled by the - protocol instance anyway.""" - tunnel_request = KnxTunnellingRequest( - communication_channel=self.communication_channel, - sequence_count=self.sequence_count, - knx_source=self.knx_source_address, - knx_destination=knx_destination) - tunnel_request.set_peer(self.transport.get_extra_info('sockname')) - return tunnel_request - - def make_configuration_request(self): - conf_request = KnxDeviceConfigurationRequest( - sockname=self.transport.get_extra_info('sockname'), - communication_channel=self.communication_channel, - sequence_count=self.sequence_count) - #conf_request.set_peer(self.transport.get_extra_info('sockname')) - return conf_request - - def knx_keep_alive(self): - """Sending CONNECTIONSTATE_REQUESTS periodically to keep the connection alive.""" - connection_state = KnxConnectionStateRequest( - sockname=self.sockname, - communication_channel=self.communication_channel) - self.transport.sendto(connection_state.get_message()) - - def knx_tunnel_disconnect(self): - """Close the tunnel connection with a DISCONNECT_REQUEST.""" - disconnect_request = KnxDisconnectRequest( - sockname=self.sockname, - communication_channel=self.communication_channel) - self.transport.sendto(disconnect_request.get_message()) - - def knx_tpci_disconnect(self, target): - tunnel_request = self.make_tunnel_request(target) - tunnel_request.tpci_unnumbered_control_data('DISCONNECT') - self.transport.sendto(tunnel_request.get_message()) - - -class KnxRoutingConnection(asyncio.DatagramProtocol): - # TODO: implement routing - """Routing is used to send KNX messages to multiple devices without any - connection setup (in contrast to tunnelling). - - * uses UDP multicast (224.0.23.12) packets to port 3671 - - * no confirmation of successful transmission - - * will send message to group address if supplied group address is known - by devices?""" - def __init__(self, future, loop=None): - self.future = future - self.loop = loop or asyncio.get_event_loop() - self.transport = None - - def connection_made(self, transport): - self.transport = transport - - def datagram_received(self, data, addr): - pass - - def _send(self, message): - self.transport.get_extra_info('socket').sendto(message.get_message(), ('224.0.23.12', 3671)) - - -class KnxBusMonitor(KnxTunnelConnection): - """Implementation of bus_monitor_mode and group_monitor_mode.""" - def __init__(self, future, loop=None, group_monitor=True): - self.future = future - self.loop = loop or asyncio.get_event_loop() - self.transport = None - self.group_monitor = group_monitor - self.tunnel_established = False - self.communication_channel = None - self.sequence_count = 0 - - def connection_made(self, transport): - self.transport = transport - self.peername = self.transport.get_extra_info('peername') - self.sockname = self.transport.get_extra_info('sockname') - if self.group_monitor: - # Create a TUNNEL_LINKLAYER layer request (default) - connect_request = KnxConnectRequest(sockname=self.sockname) - else: - # Create a TUNNEL_BUSMONITOR layer request - connect_request = KnxConnectRequest(sockname=self.sockname, layer_type=0x80) - self.transport.sendto(connect_request.get_message()) - # Send CONNECTIONSTATE_REQUEST to keep the connection alive - self.loop.call_later(50, self.knx_keep_alive) - - def datagram_received(self, data, addr): - LOGGER.debug('data: {}'.format(data)) - knx_message = parse_message(data) - - if not knx_message: - LOGGER.error('Invalid KNX message: {}'.format(data)) - self.knx_tunnel_disconnect() - self.transport.close() - self.future.set_result(None) - return - - if isinstance(knx_message, KnxConnectResponse): - if not knx_message.ERROR: - if not self.tunnel_established: - self.tunnel_established = True - self.communication_channel = knx_message.body.get('communication_channel_id') - else: - if not self.group_monitor and knx_message.ERROR_CODE == 0x23: - LOGGER.error('Device does not support BUSMONITOR, try --group-monitor instead') - else: - LOGGER.error('Connection setup error: {}'.format(knx_message.ERROR)) - self.transport.close() - self.future.set_result(None) - elif isinstance(knx_message, KnxTunnellingRequest): - self.print_message(knx_message) - if CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.con' or \ - CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.ind': - tunnelling_ack = KnxTunnellingAck( - communication_channel=knx_message.body.get('communication_channel_id'), - sequence_count=knx_message.body.get('sequence_counter')) - self.transport.sendto(tunnelling_ack.get_message()) - elif isinstance(knx_message, KnxTunnellingAck): - self.print_message(knx_message) - elif isinstance(knx_message, KnxConnectionStateResponse): - # After receiving a CONNECTIONSTATE_RESPONSE shedule the next one - self.loop.call_later(50, self.knx_keep_alive) - elif isinstance(knx_message, KnxDisconnectRequest): - connect_response = KnxDisconnectResponse(communication_channel=self.communication_channel) - self.transport.sendto(connect_response.get_message()) - self.transport.close() - self.future.set_result(None) - elif isinstance(knx_message, KnxDisconnectResponse): - self.transport.close() - self.future.set_result(None) - - def print_message(self, message): - """A generic message printing function. It defines a format for the monitoring modes.""" - assert isinstance(message, KnxTunnellingRequest) - if self.group_monitor: - format = '[ chan_id: {chan_id}, seq_no: {seq_no}, message_code: {msg_code}, \ - source_addr: {src_addr}, dest_addr: {dst_addr}, tcpi: {tcpi}, apci: {apci} ]'.format( - chan_id=message.body.get('communication_channel_id'), - seq_no=message.body.get('sequence_counter'), - msg_code=CEMI_PRIMITIVES[message.body.get('cemi').get('message_code')], - src_addr=message.parse_knx_address(message.body.get('cemi').get('knx_source')), - dst_addr=message.parse_knx_group_address(message.body.get('cemi').get('knx_destination')), - tcpi=message.body.get('cemi').get('tcpi'), - apci=message.body.get('cemi').get('apci')) - else: - format = '[ chan_id: {chan_id}, seq_no: {seq_no}, message_code: {msg_code}, \ - raw_frame: {raw_frame} ]'.format( - chan_id=message.body.get('communication_channel_id'), - seq_no=message.body.get('sequence_counter'), - msg_code=CEMI_PRIMITIVES[message.body.get('cemi').get('message_code')], - raw_frame=message.body.get('cemi').get('raw_frame')) - LOGGER.info(format) \ No newline at end of file diff --git a/libknx/manufacturers.py b/libknx/manufacturers.py deleted file mode 100644 index f2a1c0e..0000000 --- a/libknx/manufacturers.py +++ /dev/null @@ -1,8 +0,0 @@ -import json - -def get_manufacturer_by_id(id): - assert isinstance(id, int) - m = json.load(open('libknx/manufacturers.json')) - for _m in m.get('manufacturers'): - if int(_m.get('knx_manufacturer_id')) == id: - return _m.get('name') \ No newline at end of file diff --git a/libknx/scanner.py b/libknx/scanner.py deleted file mode 100644 index 957c972..0000000 --- a/libknx/scanner.py +++ /dev/null @@ -1,426 +0,0 @@ -import asyncio -import argparse -import binascii -import collections -import codecs -import logging -import os -import socket -import struct -import sys -import time -try: - # Python 3.4 - from asyncio import JoinableQueue as Queue -except ImportError: - # Python 3.5 renamed it to Queue - from asyncio import Queue - -from .core import * -from .messages import * -from .gateway import * -from .bus import * -from .manufacturers import * -from .targets import * - -__all__ = ['KnxScanner'] - -LOGGER = logging.getLogger(__name__) - - -class KnxScanner: - """The main scanner instance that takes care of scheduling workers for the targets.""" - def __init__(self, targets=None, max_workers=100, loop=None, ): - self.loop = loop or asyncio.get_event_loop() - # The number of concurrent workers for discovering KNXnet/IP gateways - self.max_workers = max_workers - # q contains all KNXnet/IP gateways - self.q = Queue(loop=self.loop) - # bus_queues is a dict containing a bus queue for each KNXnet/IP gateway - self.bus_queues = dict() - # bus_protocols is a list of all bus protocol instances for proper connection shutdown - self.bus_protocols = list() - # knx_gateways is a list of KnxTargetReport objects, one for each found KNXnet/IP gateway - self.knx_gateways = list() - # bus_devices is a list of KnxBusTargetReport objects, one for each found bus device - self.bus_devices = set() - self.bus_info = False - self.t0 = time.time() - self.t1 = None - if targets: - self.set_targets(targets) - else: - self.targets = set() - - def set_targets(self, targets): - self.targets = targets - for target in self.targets: - self.add_target(target) - - def add_target(self, target): - self.q.put_nowait(target) - - def add_bus_queue(self, gateway, bus_targets): - self.bus_queues[gateway] = Queue(loop=self.loop) - for target in bus_targets: - self.bus_queues[gateway].put_nowait(target) - return self.bus_queues[gateway] - - @asyncio.coroutine - def knx_bus_worker(self, transport, protocol, queue): - """A worker for communicating with devices on the bus.""" - try: - while True: - target = queue.get_nowait() - LOGGER.info('BUS: target: {}'.format(target)) - if not protocol.tunnel_established: - LOGGER.error('KNX tunnel is not open!') - return - - alive = yield from protocol.tpci_connect(target) - - if alive: - if not self.bus_info: - t = KnxBusTargetReport( - address=target, - type=None, - device_serial=None, - manufacturer=None) - self.bus_devices.add(t) - queue.task_done() - continue - - # TODO: the device is alive, but not probably we cannot read any properties - - # DeviceDescriptorRead - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.apci_device_descriptor_read(sequence=protocol.tpci_sequence_count) - descriptor = yield from protocol.send_data(tunnel_request.get_message(), target) - if not isinstance(descriptor, KnxTunnellingRequest) or not \ - descriptor.body.get('cemi').get('apci') == APCI_TYPES.get('A_DeviceDescriptor_Response'): - t = KnxBusTargetReport( - address=target, - type=None, - device_serial=None, - manufacturer=None) - self.bus_devices.add(t) - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.tpci_unnumbered_control_data('DISCONNECT') - protocol.send_data(tunnel_request.get_message(), target) - queue.task_done() - continue - - ret = yield from protocol.tpci_send_ncd(target) - if not ret: - LOGGER.error('ERROR OCCURED') - - dev_desc = struct.unpack('!H', descriptor.body.get('cemi').get('data'))[0] - manufacturer = None - serial = None - - if dev_desc > 0x13: - # System 1 devices do not have interface objects or a serial number - # PropertyValueRead - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.apci_property_value_read( - sequence=protocol.tpci_sequence_count, - object_index=0, - property_id=DEVICE_OBJECTS.get('PID_MANUFACTURER_ID')) - manufacturer = yield from protocol.send_data(tunnel_request.get_message(), target) - if not isinstance(manufacturer, bool): - manufacturer = manufacturer.body.get('cemi').get('data')[4:] - else: - # MemoryRead manufacturer ID - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.apci_memory_read( - sequence=protocol.tpci_sequence_count, - memory_address=0x0104, - read_count=1) - manufacturer = yield from protocol.send_data(tunnel_request.get_message(), target) - # TODO: check if it returned a proper response - if not isinstance(manufacturer, bool): - manufacturer = manufacturer.body.get('cemi').get('data')[2:] - - ret = yield from protocol.tpci_send_ncd(target) - if not ret: - manufacturer = 'COULD NOT READ MANUFACTURER' - else: - if isinstance(manufacturer, (str, bytes)): - manufacturer = int.from_bytes(manufacturer, 'big') - manufacturer = get_manufacturer_by_id(manufacturer) - - if dev_desc <= 0x13: - # MemoryRead application program - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.apci_memory_read( - sequence=protocol.tpci_sequence_count, - memory_address=0x0104, - read_count=4) - application_program = yield from protocol.send_data(tunnel_request.get_message(), target) - application_program = application_program.body.get('cemi').get('data')[2:] - yield from protocol.tpci_send_ncd(target) - - if dev_desc > 0x13: - # PropertyValueRead - # Read the serial number - tunnel_request = protocol.make_tunnel_request(target) - tunnel_request.apci_property_value_read( - sequence=protocol.tpci_sequence_count, - object_index=0, - property_id=DEVICE_OBJECTS.get('PID_SERIAL_NUMBER')) - serial = yield from protocol.send_data(tunnel_request.get_message(), target) - if not isinstance(serial, bool): - serial = serial.body.get('cemi').get('data')[4:] - - ret = yield from protocol.tpci_send_ncd(target) - if not ret: - serial = 'COULD NOT READ SERIAL' - else: - if isinstance(serial, (str, bytes)): - serial = codecs.encode(serial, 'hex').decode().upper() - - if descriptor: - t = KnxBusTargetReport( - address=target, - type=DEVICE_DESCRIPTORS.get(dev_desc) or 'Unknown', - device_serial=serial or 'Unavailable', - manufacturer=manufacturer or 'Unknown') - self.bus_devices.add(t) - - # Properly close the TPCI layer - yield from protocol.tpci_disconnect(target) - - queue.task_done() - except asyncio.CancelledError: - pass - except asyncio.QueueEmpty: - pass - - @asyncio.coroutine - def bus_scan(self, knx_gateway, bus_targets): - queue = self.add_bus_queue(knx_gateway.host, bus_targets) - LOGGER.info('Scanning {} bus device(s) on {}'.format(queue.qsize(), knx_gateway.host)) - - future = asyncio.Future() - bus_con = KnxTunnelConnection(future) - transport, bus_protocol = yield from self.loop.create_datagram_endpoint( - lambda: bus_con, remote_addr=(knx_gateway.host, knx_gateway.port)) - self.bus_protocols.append(bus_protocol) - - # Make sure the tunnel has been established - connected = yield from future - - if connected: - workers = [asyncio.Task(self.knx_bus_worker(transport, bus_protocol, queue), loop=self.loop)] - self.t0 = time.time() - yield from queue.join() - self.t1 = time.time() - for w in workers: - w.cancel() - bus_protocol.knx_tunnel_disconnect() - - for i in self.bus_devices: - knx_gateway.bus_devices.append(i) - - LOGGER.info('Bus scan took {} seconds'.format(self.t1 - self.t0)) - - @asyncio.coroutine - def knx_search_worker(self): - """Send a KnxDescription request to see if target is a KNX device.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setblocking(0) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, struct.pack('256s', str.encode(self.iface))) - - protocol = KnxGatewaySearch() - waiter = asyncio.Future(loop=self.loop) - transport = self.loop._make_datagram_transport( - sock, protocol, ('224.0.23.12', 3671), waiter) - - try: - # Wait until connection_made() has been called on the transport - yield from waiter - except: - LOGGER.error('Creating multicast transport failed!') - transport.close() - return - - # Wait SEARCH_TIMEOUT seconds for responses to our multicast packets - yield from asyncio.sleep(self.search_timeout) - - if protocol.responses: - # If protocol received SEARCH_RESPONSE packets, print them - for response in protocol.responses: - peer = response[0] - response = response[1] - - t = KnxTargetReport( - host=peer[0], - port=peer[1], - mac_address=response.body.get('dib_dev_info').get('knx_mac_address'), - knx_address=response.body.get('dib_dev_info').get('knx_address'), - device_serial=response.body.get('dib_dev_info').get('knx_device_serial'), - friendly_name=response.body.get('dib_dev_info').get('device_friendly_name'), - device_status=response.body.get('dib_dev_info').get('device_status'), - knx_medium=response.body.get('dib_dev_info').get('knx_medium'), - project_install_identifier=response.body.get('dib_dev_info').get('project_install_identifier'), - supported_services=[ - KNX_SERVICES[k] for k, v in - response.body.get('dib_supp_sv_families').get('families').items()], - bus_devices=[]) - - self.knx_gateways.append(t) - except asyncio.CancelledError: - pass - - @asyncio.coroutine - def search_gateways(self): - self.t0 = time.time() - yield from asyncio.ensure_future(asyncio.Task(self.knx_search_worker(), loop=self.loop)) - self.t1 = time.time() - LOGGER.info('Scan took {} seconds'.format(self.t1 - self.t0)) - - @asyncio.coroutine - def knx_description_worker(self): - """Send a KnxDescription request to see if target is a KNX device.""" - try: - while True: - target = self.q.get_nowait() - LOGGER.debug('Scanning {}'.format(target)) - future = asyncio.Future() - description = KnxGatewayDescription(future) - yield from self.loop.create_datagram_endpoint( - lambda: description, - remote_addr=target) - # TODO: send description responses multiple time if there is no anser after a timeout - response = yield from future - if response: - t = KnxTargetReport( - host=target[0], - port=target[1], - mac_address=response.body.get('dib_dev_info').get('knx_mac_address'), - knx_address=response.body.get('dib_dev_info').get('knx_address'), - device_serial=response.body.get('dib_dev_info').get('knx_device_serial'), - friendly_name=response.body.get('dib_dev_info').get('device_friendly_name'), - device_status=response.body.get('dib_dev_info').get('device_status'), - knx_medium=response.body.get('dib_dev_info').get('knx_medium'), - project_install_identifier=response.body.get('dib_dev_info').get('project_install_identifier'), - supported_services=[ - KNX_SERVICES[k] for k,v in - response.body.get('dib_supp_sv_families').get('families').items()], - bus_devices=[]) - - self.knx_gateways.append(t) - self.q.task_done() - except (asyncio.CancelledError, asyncio.QueueEmpty) as e: - pass - - @asyncio.coroutine - def scan(self, targets=None, search_mode=False, search_timeout=5, iface=None, - bus_targets=None, bus_info=False, bus_monitor_mode=False, group_monitor_mode=False): - """The function that will be called by run_until_complete(). This is the main coroutine.""" - if targets: - self.set_targets(targets) - - if search_mode: - self.iface = iface - self.search_timeout = search_timeout - LOGGER.info('Make sure there are no filtering rules that drop UDP multicast packets!') - yield from self.search_gateways() - for t in self.knx_gateways: - self.print_knx_target(t) - - LOGGER.info('Searching done') - - elif bus_monitor_mode or group_monitor_mode: - LOGGER.info('Starting bus monitor') - future = asyncio.Future() - bus_con = KnxBusMonitor(future, group_monitor=group_monitor_mode) - transport, self.bus_protocol = yield from self.loop.create_datagram_endpoint( - lambda: bus_con, - remote_addr=list(self.targets)[0]) - yield from future - - LOGGER.info('Stopping bus monitor') - - else: - workers = [asyncio.Task(self.knx_description_worker(), loop=self.loop) - for _ in range(self.max_workers if len(self.targets) > self.max_workers else len(self.targets))] - - self.t0 = time.time() - yield from self.q.join() - self.t1 = time.time() - for w in workers: - w.cancel() - - if bus_targets and self.knx_gateways: - self.bus_info = bus_info - bus_scanners = [asyncio.Task(self.bus_scan(g, bus_targets), loop=self.loop) for g in self.knx_gateways] - yield from asyncio.wait(bus_scanners) - else: - LOGGER.info('Scan took {} seconds'.format(self.t1 - self.t0)) - - for t in self.knx_gateways: - self.print_knx_target(t) - - @staticmethod - def print_knx_target(knx_target): - """Print a target of type KnxTargetReport in a well formatted way.""" - # TODO: make this better, and prettier. - out = dict() - out[knx_target.host] = collections.OrderedDict() - o = out[knx_target.host] - - o['Port'] = knx_target.port - o['MAC Address'] = knx_target.mac_address - o['KNX Bus Address'] = knx_target.knx_address - o['KNX Device Serial'] = knx_target.device_serial - o['KNX Medium'] = KNX_MEDIUMS.get(knx_target.knx_medium) - o['Device Friendly Name'] = binascii.b2a_qp(knx_target.friendly_name.strip()) - o['Device Status'] = knx_target.device_status - o['Project Install Identifier'] = knx_target.project_install_identifier - o['Supported Services'] = knx_target.supported_services - if knx_target.bus_devices: - o['Bus Devices'] = list() - for d in knx_target.bus_devices: - _d = dict() - _d[d.address] = collections.OrderedDict() - if d.type: - _d[d.address]['Type'] = d.type - if d.device_serial: - _d[d.address]['Device Serial'] = d.device_serial - if d.manufacturer: - _d[d.address]['Manufacturer'] = d.manufacturer - o['Bus Devices'].append(_d) - - print() - - def print_fmt(d, indent=0): - for key, value in d.items(): - if indent is 0: - print(' ' * indent + str(key)) - elif isinstance(value, (dict, collections.OrderedDict)): - if not len(value.keys()): - print(' ' * indent + str(key)) - else: - print(' ' * indent + str(key) + ': ') - else: - print(' ' * indent + str(key) + ': ', end="", flush=True) - - if key == 'Bus Devices': - print() - for i in value: - print_fmt(i, indent + 1) - elif isinstance(value, list): - for i, v in enumerate(value): - if i is 0: - print() - print(' ' * (indent + 1) + str(v)) - elif isinstance(value, (dict, collections.OrderedDict)): - print_fmt(value, indent + 1) - else: - print(value) - - print_fmt(out) - print() diff --git a/libknx/targets.py b/libknx/targets.py deleted file mode 100644 index b5c7624..0000000 --- a/libknx/targets.py +++ /dev/null @@ -1,165 +0,0 @@ -"""This module contains various helper classes that make handling targets and sets -of targets and results easiert.""" -import logging -import ipaddress -import collections - -from .messages import * - -__all__ = ['Targets', - 'KnxTargets', - 'BusResultSet', - 'KnxTargetReport', - 'KnxBusTargetReport'] - -LOGGER = logging.getLogger(__name__) - -class Targets: - """A helper class that expands provided target definitions to a list of tuples.""" - def __init__(self, targets=set(), ports=3671): - self.targets = set() - self.ports = set() - if isinstance(ports, list): - for p in ports: - self.ports.add(p) - elif isinstance(ports, int): - self.ports.add(ports) - else: - self.ports.add(3671) - - if isinstance(targets, set) or \ - isinstance(targets, list): - self._parse(targets) - - def _parse(self, targets): - """Parse all targets with ipaddress module (with CIDR notation support).""" - for target in targets: - try: - _targets = ipaddress.ip_network(target, strict=False) - except ValueError: - LOGGER.error('Invalid target definition, ignoring it: {}'.format(target)) - continue - - if '/' in target: - _targets = _targets.hosts() - - for _target in _targets: - for port in self.ports: - self.targets.add((str(_target), port)) - - -class KnxTargets: - """A helper class that expands knx bus targets to lists.""" - def __init__(self, targets): - self.targets = set() - if not targets: - self.targets = None - elif not '-' in targets and self.is_valid_physical_address(targets): - self.targets.add(targets) - else: - assert isinstance(targets, str) - if '-' in targets and targets.count('-') < 2: - # TODO: also parse dashes in octets - try: - f, t = targets.split('-') - except ValueError: - return - if not self.is_valid_physical_address(f) or \ - not self.is_valid_physical_address(t): - LOGGER.error('Invalid physical address') - # TODO: make it group address aware - elif self.physical_address_to_int(t) <= \ - self.physical_address_to_int(f): - LOGGER.error('From should be smaller then To') - else: - self.targets = self.expand_targets(f, t) - - @staticmethod - def target_gen(f, t): - f = KnxMessage.pack_knx_address(f) - t = KnxMessage.pack_knx_address(t) - for i in range(f, t + 1): - yield KnxMessage.parse_knx_address(i) - - @staticmethod - def expand_targets(f, t): - ret = set() - f = KnxMessage.pack_knx_address(f) - t = KnxMessage.pack_knx_address(t) - for i in range(f, t + 1): - ret.add(KnxMessage.parse_knx_address(i)) - return ret - - @staticmethod - def physical_address_to_int(address): - parts = address.split('.') - return (int(parts[0]) << 12) + (int(parts[1]) << 8) + (int(parts[2])) - - @staticmethod - def int_to_physical_address(address): - return '{}.{}.{}'.format((address >> 12) & 0xf, (address >> 8) & 0xf, address & 0xff) - - @staticmethod - def is_valid_physical_address(address): - assert isinstance(address, str) - try: - parts = [int(i) for i in address.split('.')] - except ValueError: - return False - if len(parts) is not 3: - return False - if (parts[0] < 1 or parts[0] > 15) or (parts[1] < 0 or parts[1] > 15): - return False - if parts[2] < 0 or parts[2] > 255: - return False - return True - - @staticmethod - def is_valid_group_address(address): - assert isinstance(address, str) - try: - parts = [int(i) for i in address.split('/')] - except ValueError: - return False - if len(parts) < 2 or len(parts) > 3: - return False - if (parts[0] < 0 or parts[0] > 15) or (parts[1] < 0 or parts[1] > 15): - return False - if len(parts) is 3: - if parts[2] < 0 or parts[2] > 255: - return False - return True - - -class BusResultSet: - # TODO: implement - - def __init__(self): - self.targets = collections.OrderedDict() - - def add(self, target): - """Add a target to the result set, at the right position.""" - pass - - -KnxTargetReport = collections.namedtuple( - 'KnxTargetReport', - ['host', - 'port', - 'mac_address', - 'knx_address', - 'device_serial', - 'friendly_name', - 'device_status', - 'knx_medium', - 'project_install_identifier', - 'supported_services', - 'bus_devices']) - - -KnxBusTargetReport = collections.namedtuple( - 'KnxBusTargetReport', - ['address', - 'type', - 'device_serial', - 'manufacturer']) \ No newline at end of file diff --git a/libknx/__init__.py b/libknxmap/__init__.py similarity index 67% rename from libknx/__init__.py rename to libknxmap/__init__.py index 78750d7..853acf9 100644 --- a/libknx/__init__.py +++ b/libknxmap/__init__.py @@ -1,6 +1,5 @@ -from .messages import * -from .bus import * -from .gateway import * +from libknxmap.data.constants import * from .core import * -from .scanner import * +from .gateway import * +from .messages import * from .targets import * \ No newline at end of file diff --git a/libknxmap/bus/__init__.py b/libknxmap/bus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libknxmap/bus/monitor.py b/libknxmap/bus/monitor.py new file mode 100644 index 0000000..7a2cc63 --- /dev/null +++ b/libknxmap/bus/monitor.py @@ -0,0 +1,104 @@ +import asyncio +import logging + +from libknxmap.bus.tunnel import KnxTunnelConnection +from libknxmap.data.constants import * +from libknxmap.messages import * + +LOGGER = logging.getLogger(__name__) + + +class KnxBusMonitor(KnxTunnelConnection): + """Implementation of bus_monitor_mode and group_monitor_mode.""" + + def __init__(self, future, loop=None, group_monitor=True): + self.future = future + self.loop = loop or asyncio.get_event_loop() + self.transport = None + self.group_monitor = group_monitor + self.tunnel_established = False + self.communication_channel = None + self.sequence_count = 0 + + def connection_made(self, transport): + self.transport = transport + self.peername = self.transport.get_extra_info('peername') + self.sockname = self.transport.get_extra_info('sockname') + if self.group_monitor: + # Create a TUNNEL_LINKLAYER layer request (default) + connect_request = KnxConnectRequest(sockname=self.sockname) + else: + # Create a TUNNEL_BUSMONITOR layer request + connect_request = KnxConnectRequest(sockname=self.sockname, layer_type='TUNNEL_BUSMONITOR') + self.transport.sendto(connect_request.get_message()) + # Send CONNECTIONSTATE_REQUEST to keep the connection alive + self.loop.call_later(50, self.knx_keep_alive) + + def datagram_received(self, data, addr): + knx_message = parse_message(data) + + if not knx_message: + LOGGER.error('Invalid KNX message: {}'.format(data)) + self.knx_tunnel_disconnect() + self.transport.close() + self.future.set_result(None) + return + + if isinstance(knx_message, KnxConnectResponse): + if not knx_message.ERROR: + if not self.tunnel_established: + self.tunnel_established = True + self.communication_channel = knx_message.body.get('communication_channel_id') + else: + if not self.group_monitor and knx_message.ERROR_CODE == 0x23: + LOGGER.error('Device does not support BUSMONITOR, try --group-monitor instead') + else: + LOGGER.error('Connection setup error: {}'.format(knx_message.ERROR)) + self.transport.close() + self.future.set_result(None) + elif isinstance(knx_message, KnxTunnellingRequest): + self.print_message(knx_message) + if CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.con' or \ + CEMI_PRIMITIVES[knx_message.body.get('cemi').get('message_code')] == 'L_Data.ind': + tunnelling_ack = KnxTunnellingAck( + communication_channel=knx_message.body.get('communication_channel_id'), + sequence_count=knx_message.body.get('sequence_counter')) + self.transport.sendto(tunnelling_ack.get_message()) + elif isinstance(knx_message, KnxTunnellingAck): + self.print_message(knx_message) + elif isinstance(knx_message, KnxConnectionStateResponse): + # After receiving a CONNECTIONSTATE_RESPONSE shedule the next one + self.loop.call_later(50, self.knx_keep_alive) + elif isinstance(knx_message, KnxDisconnectRequest): + connect_response = KnxDisconnectResponse(communication_channel=self.communication_channel) + self.transport.sendto(connect_response.get_message()) + self.transport.close() + self.future.set_result(None) + elif isinstance(knx_message, KnxDisconnectResponse): + self.transport.close() + self.future.set_result(None) + + def print_message(self, message): + """A generic message printing function. It defines a format for the monitoring modes.""" + assert isinstance(message, KnxTunnellingRequest) + if self.group_monitor: + format = ('[ chan_id: {chan_id}, seq_no: {seq_no}, message_code: {msg_code}, ' + 'source_addr: {src_addr}, dest_addr: {dst_addr}, tpci_type: {tpci_type}, ' + 'tpci_seq: {tpci_seq}, apci_type: {apci_type}, apci_data: {apci_data} ]').format( + chan_id=message.body.get('communication_channel_id'), + seq_no=message.body.get('sequence_counter'), + msg_code=CEMI_PRIMITIVES[message.body.get('cemi').get('message_code')], + src_addr=message.parse_knx_address(message.body.get('cemi').get('knx_source')), + dst_addr=message.parse_knx_group_address(message.body.get('cemi').get('knx_destination')), + tpci_type=_CEMI_TPCI_TYPES.get(message.body.get('cemi').get('tpci').get('type')), + tpci_seq=message.body.get('cemi').get('tpci').get('sequence'), + apci_type=_CEMI_APCI_TYPES.get(message.body.get('cemi').get('apci').get('type')), + apci_data=message.body.get('cemi').get('apci').get('data')) + else: + format = ('[ chan_id: {chan_id}, seq_no: {seq_no}, message_code: {msg_code}, ' + 'raw_frame: {raw_frame} ]').format( + chan_id=message.body.get('communication_channel_id'), + seq_no=message.body.get('sequence_counter'), + msg_code=CEMI_PRIMITIVES[message.body.get('cemi').get('message_code')], + raw_frame=message.body.get('cemi').get('raw_frame')) + LOGGER.info(format) diff --git a/libknx/objectserver.py b/libknxmap/bus/objectserver.py similarity index 83% rename from libknx/objectserver.py rename to libknxmap/bus/objectserver.py index b60ace9..ee0188b 100644 --- a/libknx/objectserver.py +++ b/libknxmap/bus/objectserver.py @@ -6,4 +6,4 @@ ObjectServer can either be included in KNXnet/IP packets (with an appropriate connection type, or via TCP on port 12004.""" -# TODO: ip control runs a objectserver on TCP port 12004 \ No newline at end of file +# TODO: ip control runs a objectserver on TCP port 12004 diff --git a/libknxmap/bus/router.py b/libknxmap/bus/router.py new file mode 100644 index 0000000..669f8cd --- /dev/null +++ b/libknxmap/bus/router.py @@ -0,0 +1,29 @@ +import asyncio +import logging + +from libknxmap.data.constants import * +from libknxmap.messages import * + +LOGGER = logging.getLogger(__name__) + + +class KnxRoutingConnection(asyncio.DatagramProtocol): + # TODO: implement routing + """Routing is used to send KNX messages to multiple devices without any + connection setup (in contrast to tunnelling).""" + + def __init__(self, target, value, loop=None): + self.loop = loop or asyncio.get_event_loop() + self.transport = None + self.target = target + self.value = value + + def connection_made(self, transport): + self.transport = transport + self.peername = self.transport.get_extra_info('peername') + self.sockname = self.transport.get_extra_info('sockname') + packet = KnxRoutingIndication(knx_destination=self.target) + packet.apci_group_value_write(value=self.value) + self.transport.get_extra_info('socket').sendto(packet.get_message(), + (KNX_CONSTANTS.get('MULTICAST_ADDR'), + KNX_CONSTANTS.get('DEFAULT_PORT'))) diff --git a/libknxmap/bus/tunnel.py b/libknxmap/bus/tunnel.py new file mode 100644 index 0000000..7fa3dd7 --- /dev/null +++ b/libknxmap/bus/tunnel.py @@ -0,0 +1,434 @@ +import asyncio +import logging + +from libknxmap.data.constants import * +from libknxmap.messages import * + +LOGGER = logging.getLogger(__name__) + + +class KnxTunnelConnection(asyncio.DatagramProtocol): + """Communicate with bus devices via a KNX gateway using TunnellingRequests. A tunneling + connection is always used if the bus destination is a physical KNX address.""" + + def __init__(self, future, connection_type=0x04, layer_type='TUNNEL_LINKLAYER', loop=None): + self.future = future + self.connection_type = connection_type + self.layer_type = layer_type + self.target_futures = dict() + self.loop = loop or asyncio.get_event_loop() + self.transport = None + self.tunnel_established = False + self.communication_channel = None + self.sequence_count = 0 # sequence counter in KNX body + self.tpci_seq_counts = dict() # NCD/NPD counter for each TPCI connection + self.knx_source_address = None # TODO: is the actual address needed? or just 0.0.0? + self.response_queue = list() + + def connection_made(self, transport): + """The connection setup function that takes care of: + + * Sending a KnxConnectRequest + * Schedule KnxConnectionStateRequests + * Schedule response queue polling""" + self.transport = transport + self.peername = self.transport.get_extra_info('peername') + self.sockname = self.transport.get_extra_info('sockname') + connect_request = KnxConnectRequest( + sockname=self.sockname, + connection_type=self.connection_type, + layer_type=self.layer_type) + self.transport.sendto(connect_request.get_message()) + # Schedule CONNECTIONSTATE_REQUEST to keep the connection alive + self.loop.call_later(50, self.knx_keep_alive) + self.loop.call_later(4, self.poll_response_queue) + + def poll_response_queue(self): + """Check if there if there is a KNX message for a + target that arrived out-of-band.""" + if self.response_queue: + for response in self.response_queue: + knx_src = response.parse_knx_address(response.body.get('cemi').get('knx_source')) + knx_dst = response.parse_knx_address(response.body.get('cemi').get('knx_destination')) + if not knx_src and not knx_dst: + continue + + if knx_dst in self.target_futures.keys(): + if not self.target_futures[knx_dst].done(): + self.target_futures[knx_dst].set_result(response) + del self.target_futures[knx_dst] + elif knx_src in self.target_futures.keys(): + if not self.target_futures[knx_src].done(): + self.target_futures[knx_src].set_result(response) + del self.target_futures[knx_src] + # Reschedule polling + self.loop.call_later(2, self.poll_response_queue) + + def process_target(self, target, value, knx_msg=None): + """When a L_Data.con NDP request request arrives after + e.g. a A_DeviceDescriptor_Read, check if we get a + L_Data.ind NDP request with the actual data. + + Note: Between a L_Data.con NDP and a L_Data.ind NDP + there will most likely (pretty sure) be a L_Data.ind + NCD request.""" + if target in self.target_futures.keys(): + if not self.target_futures[target].done(): + self.target_futures[target].set_result(value) + del self.target_futures[target] + else: + if isinstance(value, KnxMessage): + # If value is a KnxMessage itself, + # jsut add it to the response_queue. + self.response_queue.append(value) + elif knx_msg and isinstance(knx_msg, KnxMessage): + # If knx_msg is set, append it to + # the response_queue. + self.response_queue.append(knx_msg) + + def datagram_received(self, data, addr): + """This function gets called whenever a data packet is received. It + will try to parse the incoming KNX message and delegate further + processing to the corresponding service handler (see KNX_SERVICES in + the core module).""" + knx_msg = parse_message(data) + if not knx_msg: + LOGGER.error('Invalid KNX message: {}'.format(data)) + self.knx_tunnel_disconnect() + self.transport.close() + self.future.set_result(None) + return + knx_service_type = knx_msg.header.get('service_type') >> 8 + if knx_service_type is 0x02: # Core + self.handle_core_services(knx_msg) + elif knx_service_type is 0x03: # Device Management + self.handle_configuration_services(knx_msg) + elif knx_service_type is 0x04: # Tunnelling + self.handle_tunnel_services(knx_msg) + else: + LOGGER.error('Service not implemented: {}'.format(KNX_SERVICES.get(knx_service_type))) + + def handle_core_services(self, knx_msg): + if isinstance(knx_msg, KnxConnectResponse): + if not knx_msg.ERROR: + if not self.tunnel_established: + self.tunnel_established = True + self.communication_channel = knx_msg.body.get('communication_channel_id') + self.knx_source_address = knx_msg.body.get('data_block').get('knx_address') + self.future.set_result(True) + else: + LOGGER.error(knx_msg.ERROR) + self.transport.close() + self.future.set_result(None) + elif isinstance(knx_msg, KnxConnectionStateResponse): + # After receiving a CONNECTIONSTATE_RESPONSE schedule the next one + self.loop.call_later(50, self.knx_keep_alive) + elif isinstance(knx_msg, KnxDisconnectRequest): + disconnect_response = KnxDisconnectResponse(communication_channel=self.communication_channel) + self.transport.sendto(disconnect_response.get_message()) + self.transport.close() + if not self.future.done(): + self.future.set_result(None) + elif isinstance(knx_msg, KnxDisconnectResponse): + self.transport.close() + if not self.future.done(): + self.future.set_result(None) + else: + LOGGER.error('Unknown Core Message: {}'.format(knx_msg.header.get('service_type'))) + + def handle_configuration_services(self, knx_msg): + if isinstance(knx_msg, KnxDeviceConfigurationRequest): + conf_ack = KnxDeviceConfigurationAck( + communication_channel=knx_msg.body.get('communication_channel_id'), + sequence_count=knx_msg.body.get('sequence_counter')) + self.transport.sendto(conf_ack.get_message()) + else: + LOGGER.error('Unknown Configuration Message: {}'.format(knx_msg.header.get('service_type'))) + + def handle_tunnel_services(self, knx_msg): + if isinstance(knx_msg, KnxTunnellingRequest): + knx_src = knx_msg.parse_knx_address(knx_msg.body.get('cemi').get('knx_source')) + knx_dst = knx_msg.parse_knx_address(knx_msg.body.get('cemi').get('knx_destination')) + cemi_msg_code = knx_msg.body.get('cemi').get('message_code') + cemi_tpci_type = knx_msg.body.get('cemi').get('tpci').get('type') + cemi_apci_type = None + if knx_msg.body.get('cemi').get('apci'): + cemi_apci_type = knx_msg.body.get('cemi').get('apci').get('type') + + LOGGER.debug(('[KnxTunnellingRequest] SRC: {knx_src}, DST: {knx_dst}, CODE: {msg_code}, ' + 'SEQ: {seq}. TPCI: {tpci}, APCI: {apci}').format( + knx_src=knx_src, + knx_dst=knx_dst, + msg_code=_CEMI_MSG_CODES.get(cemi_msg_code), + seq=knx_msg.body.get('cemi').get('tpci').get('sequence'), + tpci=_CEMI_TPCI_TYPES.get(cemi_tpci_type), + apci=_CEMI_APCI_TYPES.get(cemi_apci_type))) + + if cemi_msg_code == CEMI_MSG_CODES.get('L_Data.con'): + # TODO: is this for NCD's even necessary? + if cemi_tpci_type in [CEMI_TPCI_TYPES.get('UCD'), CEMI_TPCI_TYPES.get('NCD')]: + # This could be e.g. a response for a tcpi_connect() or + # tpci_send_ncd() message. For these messages the return + # value should be boolean to indicate that either a + # address is not in use/device is not available (UCD) + # or an error happened (NCD). + if knx_msg.body.get('cemi').get('controlfield_1').get('confirm'): + # If the confirm flag is set, the device is not alive + self.process_target(knx_dst, False, knx_msg) + else: + # If the confirm flag is not set, the device is alive + self.process_target(knx_dst, True, knx_msg) + + if cemi_tpci_type == CEMI_TPCI_TYPES.get('UCD'): + # For each alive device, create a new sequence counter + self.tpci_seq_counts[knx_dst] = 0 + + elif cemi_tpci_type == CEMI_TPCI_TYPES.get('NDP'): + # If we get a confirmation for our device descriptor request, + # check if L_Data.ind arrives. + if cemi_apci_type in [CEMI_APCI_TYPES.get('A_DeviceDescriptor_Read'), + CEMI_APCI_TYPES.get('A_PropertyValue_Read')]: + self.loop.call_later(3, self.process_target, knx_dst, False, knx_msg) + + elif cemi_tpci_type == CEMI_TPCI_TYPES.get('UDP'): + # After e.g. an A_GroupValue_Write we just get a + # L_Data.con for a UDP. + if knx_dst in self.target_futures.keys() and \ + not self.target_futures[knx_dst].done(): + self.target_futures[knx_dst].set_result(False) + del self.target_futures[knx_dst] + else: + self.response_queue.append(knx_msg) + + elif cemi_msg_code == CEMI_MSG_CODES.get('L_Data.ind'): + + if cemi_tpci_type == CEMI_TPCI_TYPES.get('UCD'): + + if knx_msg.body.get('cemi').get('tpci').get('status') is 1: + # TODO: why checking status here? pls document why + if knx_dst in self.target_futures.keys(): + if not self.target_futures[knx_dst].done(): + self.target_futures[knx_dst].set_result(False) + del self.target_futures[knx_dst] + else: + self.response_queue.append(knx_msg) + + elif cemi_tpci_type == CEMI_TPCI_TYPES.get('NCD'): + # If we sent e.g. a A_DeviceDescriptor_Read, this + # would arrive right before the actual data. + # TODO: if something fails, can we see it in this message? + pass + + elif cemi_tpci_type == CEMI_TPCI_TYPES.get('NDP'): + + if cemi_apci_type == CEMI_APCI_TYPES.get('A_DeviceDescriptor_Response'): + LOGGER.debug('{knx_src}: DEVICEDESCRIPTOR_RESPONSE DATA: {data}'.format( + knx_src=knx_src, + data=knx_msg.body.get('cemi').get('data'))) + + elif cemi_apci_type == CEMI_APCI_TYPES.get('A_Authorize_Response'): + LOGGER.debug('{knx_src}: AUTHORIZE_RESPONSE DATA: {data}'.format( + knx_src=knx_src, + data=knx_msg.body.get('cemi').get('data'))) + + elif cemi_apci_type == CEMI_APCI_TYPES.get('A_PropertyValue_Response'): + LOGGER.debug('{peer}/{knx_source}/{knx_dest}: PROPERTY_VALUE_RESPONSE DATA: {data}'.format( + peer=self.peername[0], + knx_source=knx_src, + knx_dest=knx_dst, + data=knx_msg.body.get('cemi').get('data')[4:])) + + elif cemi_apci_type == CEMI_APCI_TYPES.get('A_Memory_Response'): + LOGGER.debug('{peer}/{knx_src}: MEMORY_RESPONSE DATA: {data}'.format( + peer=self.peername[0], + knx_src=knx_src, + data=knx_msg.body.get('cemi').get('data'))) + + # If we receive any Numbered Data Packets for + # targets without futures, add the knx_source + # to the response queue for later processing. + self.process_target(knx_src, knx_msg) + + # If we receive any L_Data.con or L_Data.ind from a KNXnet/IP gateway + # we have to reply with a tunnelling ack. + if cemi_msg_code in [CEMI_MSG_CODES.get('L_Data.con'), CEMI_MSG_CODES.get('L_Data.ind')]: + tunnelling_ack = KnxTunnellingAck( + communication_channel=knx_msg.body.get('communication_channel_id'), + sequence_count=knx_msg.body.get('sequence_counter')) + self.transport.sendto(tunnelling_ack.get_message()) + + elif isinstance(knx_msg, KnxTunnellingAck): + # TODO: do we have to increase any sequence here? + LOGGER.debug('Tunnelling ACK reqceived') + else: + LOGGER.error('Unknown Tunnelling Message: {}'.format(knx_msg.header.get('service_type'))) + + def send_data(self, data, target=None): + """A wrapper for sendto() that takes care of incrementing the sequence counter. + + Note: the sequence counter field is only 1 byte. After incrementing the counter + to 255, it seems to be OK to just start over from 0. At least this applies + to the tested devices.""" + f = asyncio.Future() + if target: + self.target_futures[target] = f + self.transport.sendto(data) + if self.sequence_count == 255: + self.sequence_count = 0 + else: + self.sequence_count += 1 + return f + + def tpci_connect(self, target): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.tpci_unnumbered_control_data('CONNECT') + return self.send_data(tunnel_request.get_message(), target) + + def tpci_disconnect(self, target): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.tpci_unnumbered_control_data('DISCONNECT') + return self.send_data(tunnel_request.get_message(), target) + + def tpci_send_ncd(self, target): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.tpci_numbered_control_data('ACK', sequence=self.tpci_seq_counts.get(target)) + # increment TPCI sequence counter + if self.tpci_seq_counts.get(target) == 15: + self.tpci_seq_counts[target] = 0 + else: + self.tpci_seq_counts[target] += 1 + return self.send_data(tunnel_request.get_message(), target) + + def make_tunnel_request(self, knx_dst): + """A helper function that returns a KnxTunnellingRequest that is already predefined + for the current tunnel connection. It already sets the communication channel, the + sequence count, the KNX source address and the peer which are all handled by the + protocol instance anyway.""" + tunnel_request = KnxTunnellingRequest( + communication_channel=self.communication_channel, + sequence_count=self.sequence_count, + knx_source=self.knx_source_address, + knx_destination=knx_dst) + tunnel_request.set_peer(self.transport.get_extra_info('sockname')) + return tunnel_request + + def make_configuration_request(self): + conf_request = KnxDeviceConfigurationRequest( + sockname=self.transport.get_extra_info('sockname'), + communication_channel=self.communication_channel, + sequence_count=self.sequence_count) + # conf_request.set_peer(self.transport.get_extra_info('sockname')) + return conf_request + + def knx_keep_alive(self): + """Sending CONNECTIONSTATE_REQUESTS periodically to + keep the tunnel alive.""" + connection_state = KnxConnectionStateRequest( + sockname=self.sockname, + communication_channel=self.communication_channel) + self.transport.sendto(connection_state.get_message()) + + def knx_tunnel_disconnect(self): + """Close the tunnel connection with a DISCONNECT_REQUEST.""" + disconnect_request = KnxDisconnectRequest( + sockname=self.sockname, + communication_channel=self.communication_channel) + self.transport.sendto(disconnect_request.get_message()) + + def knx_tpci_disconnect(self, target): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.tpci_unnumbered_control_data('DISCONNECT') + self.transport.sendto(tunnel_request.get_message()) + + @asyncio.coroutine + def apci_device_descriptor_read(self, target): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_device_descriptor_read( + sequence=self.tpci_seq_counts.get(target)) + value = yield from self.send_data(tunnel_request.get_message(), target) + yield from self.tpci_send_ncd(target) + cemi = value.body.get('cemi') + if isinstance(value, KnxTunnellingRequest) and \ + cemi.get('apci').get('type') == CEMI_APCI_TYPES.get('A_DeviceDescriptor_Response') and \ + cemi.get('data'): + return value.body.get('cemi').get('data') + else: + return False + + @asyncio.coroutine + def apci_property_value_read(self, target, object_index=0, property_id=0x0f, + num_elements=1, start_index=1): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_property_value_read( + sequence=self.tpci_seq_counts.get(target), + object_index=object_index, + property_id=property_id, + num_elements=num_elements, + start_index=start_index) + value = yield from self.send_data(tunnel_request.get_message(), target) + yield from self.tpci_send_ncd(target) + if isinstance(value, KnxTunnellingRequest) and \ + value.body.get('cemi').get('data'): + return value.body.get('cemi').get('data')[4:] + else: + return False + + @asyncio.coroutine + def apci_property_description_read(self, target, object_index=0, property_id=0x0f, + num_elements=1, start_index=1): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_property_description_read( + sequence=self.tpci_seq_counts.get(target), + object_index=object_index, + property_id=property_id, + num_elements=num_elements, + start_index=start_index) + value = yield from self.send_data(tunnel_request.get_message(), target) + yield from self.tpci_send_ncd(target) + if isinstance(value, KnxTunnellingRequest) and \ + value.body.get('cemi').get('data'): + return value.body.get('cemi').get('data')[4:] + else: + return False + + @asyncio.coroutine + def apci_memory_read(self, target, memory_address=0x0060, read_count=1): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_memory_read( + sequence=self.tpci_seq_counts.get(target), + memory_address=memory_address, + read_count=read_count) + value = yield from self.send_data(tunnel_request.get_message(), target) + yield from self.tpci_send_ncd(target) + if isinstance(value, KnxTunnellingRequest) and \ + value.body.get('cemi').get('data'): + return value.body.get('cemi').get('data')[2:] + else: + return False + + @asyncio.coroutine + def apci_authenticate(self, target, key=0xffffffff): + """Send an A_Authorize_Request to target with the + supplied key. Returns the access level as an int + or False if an error occurred.""" + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_authorize_request( + sequence=self.tpci_seq_counts.get(target), + key=key) + auth = yield from self.send_data(tunnel_request.get_message(), target) + yield from self.tpci_send_ncd(target) + if isinstance(auth, KnxTunnellingRequest): + return int.from_bytes(auth.body.get('cemi').get('data'), 'big') + else: + return False + + @asyncio.coroutine + def apci_group_value_write(self, target, value=0): + tunnel_request = self.make_tunnel_request(target) + tunnel_request.apci_group_value_write(value=value) + value = yield from self.send_data(tunnel_request.get_message(), target) + if isinstance(value, KnxTunnellingRequest) and \ + value.body.get('cemi').get('data'): + return value.body.get('cemi').get('data')[4:] + else: + return False diff --git a/libknxmap/core.py b/libknxmap/core.py new file mode 100644 index 0000000..7b00039 --- /dev/null +++ b/libknxmap/core.py @@ -0,0 +1,516 @@ +import asyncio +import codecs +import collections +import functools +import logging +import socket +import struct +import time + +try: + # Python 3.4 + from asyncio import JoinableQueue as Queue +except ImportError: + # Python 3.5 renamed it to Queue + from asyncio import Queue + +from libknxmap.data.constants import * +from libknxmap.messages import * +from libknxmap.gateway import * +from libknxmap.manufacturers import * +from libknxmap.targets import * +from libknxmap.bus.tunnel import KnxTunnelConnection +from libknxmap.bus.router import KnxRoutingConnection +from libknxmap.bus.monitor import KnxBusMonitor + +__all__ = ['KnxMap'] + +LOGGER = logging.getLogger(__name__) + + +class KnxMap: + """The main scanner instance that takes care of scheduling workers for the targets.""" + + def __init__(self, targets=None, max_workers=100, loop=None): + self.loop = loop or asyncio.get_event_loop() + # The number of concurrent workers for discovering KNXnet/IP gateways + self.max_workers = max_workers + # q contains all KNXnet/IP gateways + self.q = Queue(loop=self.loop) + # bus_queues is a dict containing a bus queue for each KNXnet/IP gateway + self.bus_queues = dict() + # bus_protocols is a list of all bus protocol instances for proper connection shutdown + self.bus_protocols = list() + # knx_gateways is a list of KnxTargetReport objects, one for each found KNXnet/IP gateway + self.knx_gateways = list() + # bus_devices is a list of KnxBusTargetReport objects, one for each found bus device + self.bus_devices = set() + self.bus_info = False + self.t0 = time.time() + self.t1 = None + if targets: + self.set_targets(targets) + else: + self.targets = set() + + def set_targets(self, targets): + self.targets = targets + for target in self.targets: + self.add_target(target) + + def add_target(self, target): + self.q.put_nowait(target) + + def add_bus_queue(self, gateway, bus_targets): + self.bus_queues[gateway] = Queue(loop=self.loop) + for target in bus_targets: + self.bus_queues[gateway].put_nowait(target) + return self.bus_queues[gateway] + + @asyncio.coroutine + def bruteforce_auth_key(self, knx_gateway, target): + if isinstance(target, set): + target = list(target)[0] + future = asyncio.Future() + transport, protocol = yield from self.loop.create_datagram_endpoint( + functools.partial(KnxTunnelConnection, future), + remote_addr=(knx_gateway[0], knx_gateway[1])) + self.bus_protocols.append(protocol) + + # Make sure the tunnel has been established + connected = yield from future + alive = yield from protocol.tpci_connect(target) + + # Bruteforce the key via A_Authorize_Request messages + # for key in range(0, 0xffffffff): + for key in [0x11223344, 0x12345678, 0x00000000, 0x87654321, 0x11111111, 0xffffffff]: + access_level = yield from protocol.apci_authenticate(target, key) + if access_level == 0: + print("GOT THE KEY: {}".format(format(key, '08x'))) + break + + @asyncio.coroutine + def knx_bus_worker(self, transport, protocol, queue): + """A worker for communicating with devices on the bus.""" + try: + while True: + target = queue.get_nowait() + LOGGER.info('BUS: target: {}'.format(target)) + if not protocol.tunnel_established: + LOGGER.error('KNX tunnel is not open!') + return + + alive = yield from protocol.tpci_connect(target) + + if alive: + properties = collections.OrderedDict() + serial = None + + # DeviceDescriptorRead + descriptor = yield from protocol.apci_device_descriptor_read(target) + if not descriptor: + tunnel_request = protocol.make_tunnel_request(target) + tunnel_request.tpci_unnumbered_control_data('DISCONNECT') + protocol.send_data(tunnel_request.get_message(), target) + queue.task_done() + continue + + if not self.bus_info: + t = KnxBusTargetReport(address=target) + self.bus_devices.add(t) + tunnel_request = protocol.make_tunnel_request(target) + tunnel_request.tpci_unnumbered_control_data('DISCONNECT') + protocol.send_data(tunnel_request.get_message(), target) + queue.task_done() + continue + + dev_desc = struct.unpack('!H', descriptor)[0] + desc_medium, desc_type, desc_version = KnxMessage.parse_device_descriptor(dev_desc) + + if desc_type > 1: + # Read System 2 and System 7 manufacturer ID object + manufacturer = yield from protocol.apci_property_value_read( + target, + property_id=DEVICE_OBJECTS.get('PID_MANUFACTURER_ID')) + if isinstance(manufacturer, (str, bytes)): + manufacturer = int.from_bytes(manufacturer, 'big') + manufacturer = get_manufacturer_by_id(manufacturer) + + # Read the device state + device_state = yield from protocol.apci_memory_read( + target, + memory_address=0x0060) + if device_state: + properties['DEVICE_STATE'] = KnxMessage.unpack_cemi_runstate( + int.from_bytes(device_state, 'big')) + + # Read the serial number object on System 2 and System 7 devices + serial = yield from protocol.apci_property_value_read( + target, + property_id=DEVICE_OBJECTS.get('PID_SERIAL_NUMBER')) + if isinstance(serial, (str, bytes)): + serial = codecs.encode(serial, 'hex').decode().upper() + + # DEV - group value write + # r = yield from protocol.apci_group_value_write('0.0.4', value=1) + # r = yield from protocol.apci_group_value_write('0.0.4', value=0) + # r = yield from protocol.apci_group_value_write('0.0.4', value=1) + # r = yield from protocol.apci_group_value_write('0.0.4', value=0) + # r = yield from protocol.apci_group_value_write('0.0.4', value=1) + # r = yield from protocol.apci_group_value_write('0.0.4', value=0) + + # If we want to authenticate + # auth_level = yield from protocol.apci_authenticate( + # target, + # key=self.auth_key) + + for object_index, props in OBJECTS.items(): + x = collections.OrderedDict() + for k, v in props.items(): + ret = yield from protocol.apci_property_value_read( + target, + property_id=v, + object_index=object_index) + if ret: + x[k.replace('PID_', '')] = codecs.encode(ret, 'hex') + if x: + properties[OBJECT_TYPES.get(object_index)] = x + + else: + # Try to MemoryRead the manufacturer ID on System 1 devices. + # Note: System 1 devices do not support access controls, so + # an authorization request is not needed. + manufacturer = yield from protocol.apci_memory_read( + target, + memory_address=0x0104, + read_count=1) + if isinstance(manufacturer, (str, bytes)): + manufacturer = int.from_bytes(manufacturer, 'big') + manufacturer = get_manufacturer_by_id(manufacturer) + + device_state = yield from protocol.apci_memory_read( + target, + memory_address=0x0060) + if device_state: + properties['DEVICE_STATE'] = codecs.encode(device_state, 'hex') + + ret = yield from protocol.apci_memory_read( + target, + memory_address=0x0105, + read_count=2) + if ret: + properties['DevTyp'] = codecs.encode(ret, 'hex') + + ret = yield from protocol.apci_memory_read( + target, + memory_address=0x0101, + read_count=3) + if ret: + properties['ManData'] = codecs.encode(ret, 'hex') + + ret = yield from protocol.apci_memory_read( + target, + memory_address=0x0108, + read_count=1) + if ret: + properties['CheckLim'] = codecs.encode(ret, 'hex') + + ret = yield from protocol.apci_memory_read( + target, + memory_address=0x01FE, + read_count=1) + if ret: + properties['UsrPrg'] = codecs.encode(ret, 'hex') + + ret = yield from protocol.apci_memory_read( + target, + memory_address=0x0116, + read_count=4) + if ret: + properties['AdrTab'] = codecs.encode(ret, 'hex') + + start_addr = 0x0100 + properties['EEPROM_DUMP'] = b'' + for i in range(51): + ret = yield from protocol.apci_memory_read( + target, + memory_address=start_addr, + read_count=5) + if ret: + properties['EEPROM_DUMP'] += codecs.encode(ret, 'hex') + start_addr += 5 + + if descriptor: + t = KnxBusTargetReport( + address=target, + medium=desc_medium, + type=desc_type, + version=desc_version, + device_serial=serial, + manufacturer=manufacturer, + properties=properties) + self.bus_devices.add(t) + + # Properly close the TPCI layer + yield from protocol.tpci_disconnect(target) + + queue.task_done() + except asyncio.CancelledError: + pass + except asyncio.QueueEmpty: + pass + + @asyncio.coroutine + def bus_scan(self, knx_gateway, bus_targets): + queue = self.add_bus_queue(knx_gateway.host, bus_targets) + LOGGER.info('Scanning {} bus device(s) on {}'.format(queue.qsize(), knx_gateway.host)) + future = asyncio.Future() + transport, bus_protocol = yield from self.loop.create_datagram_endpoint( + functools.partial(KnxTunnelConnection, future), + remote_addr=(knx_gateway.host, knx_gateway.port)) + self.bus_protocols.append(bus_protocol) + + # Make sure the tunnel has been established + connected = yield from future + + if connected: + workers = [asyncio.Task(self.knx_bus_worker(transport, bus_protocol, queue), loop=self.loop)] + self.t0 = time.time() + yield from queue.join() + self.t1 = time.time() + for w in workers: + w.cancel() + bus_protocol.knx_tunnel_disconnect() + + for i in self.bus_devices: + knx_gateway.bus_devices.append(i) + + LOGGER.info('Bus scan took {} seconds'.format(self.t1 - self.t0)) + + @asyncio.coroutine + def knx_search_worker(self): + """Send a KnxSearch request to see if target is a KNX device.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, struct.pack('256s', str.encode(self.iface))) + + protocol = KnxGatewaySearch() + waiter = asyncio.Future(loop=self.loop) + transport = self.loop._make_datagram_transport( + sock, protocol, ('224.0.23.12', 3671), waiter) + + try: + # Wait until connection_made() has been called on the transport + yield from waiter + except: + LOGGER.error('Creating multicast transport failed!') + transport.close() + return + + # Wait SEARCH_TIMEOUT seconds for responses to our multicast packets + yield from asyncio.sleep(self.search_timeout) + + if protocol.responses: + # If protocol received SEARCH_RESPONSE packets, print them + for response in protocol.responses: + peer = response[0] + response = response[1] + t = KnxTargetReport( + host=peer[0], + port=peer[1], + mac_address=response.body.get('dib_dev_info').get('knx_mac_address'), + knx_address=response.body.get('dib_dev_info').get('knx_address'), + device_serial=response.body.get('dib_dev_info').get('knx_device_serial'), + friendly_name=response.body.get('dib_dev_info').get('device_friendly_name'), + device_status=response.body.get('dib_dev_info').get('device_status'), + knx_medium=response.body.get('dib_dev_info').get('knx_medium'), + project_install_identifier=response.body.get('dib_dev_info').get('project_install_identifier'), + supported_services=[ + KNX_SERVICES[k] for k, v in + response.body.get('dib_supp_sv_families').get('families').items()], + bus_devices=[]) + + self.knx_gateways.append(t) + except asyncio.CancelledError: + pass + + @asyncio.coroutine + def search_gateways(self): + self.t0 = time.time() + yield from asyncio.ensure_future(asyncio.Task(self.knx_search_worker(), loop=self.loop)) + self.t1 = time.time() + LOGGER.info('Scan took {} seconds'.format(self.t1 - self.t0)) + + @asyncio.coroutine + def knx_description_worker(self): + """Send a KnxDescription request to see if target is a KNX device.""" + try: + while True: + target = self.q.get_nowait() + LOGGER.debug('Scanning {}'.format(target)) + for _try in range(self.desc_retries): + LOGGER.debug('Sending {}. KnxDescriptionRequest to {}'.format(_try, target)) + future = asyncio.Future() + yield from self.loop.create_datagram_endpoint( + functools.partial(KnxGatewayDescription, future, timeout=self.desc_timeout), + remote_addr=target) + response = yield from future + if response: + break + + if response and isinstance(response, KnxDescriptionResponse): + t = KnxTargetReport( + host=target[0], + port=target[1], + mac_address=response.body.get('dib_dev_info').get('knx_mac_address'), + knx_address=response.body.get('dib_dev_info').get('knx_address'), + device_serial=response.body.get('dib_dev_info').get('knx_device_serial'), + friendly_name=response.body.get('dib_dev_info').get('device_friendly_name'), + device_status=response.body.get('dib_dev_info').get('device_status'), + knx_medium=response.body.get('dib_dev_info').get('knx_medium'), + project_install_identifier=response.body.get('dib_dev_info').get('project_install_identifier'), + supported_services=[ + KNX_SERVICES[k] for k, v in + response.body.get('dib_supp_sv_families').get('families').items()], + bus_devices=[]) + + self.knx_gateways.append(t) + self.q.task_done() + except (asyncio.CancelledError, asyncio.QueueEmpty): + pass + + @asyncio.coroutine + def monitor(self, targets=None, group_monitor_mode=False): + if targets: + self.set_targets(targets) + if group_monitor_mode: + LOGGER.debug('Starting group monitor') + else: + LOGGER.debug('Starting bus monitor') + future = asyncio.Future() + transport, protocol = yield from self.loop.create_datagram_endpoint( + functools.partial(KnxBusMonitor, future, group_monitor=group_monitor_mode), + remote_addr=list(self.targets)[0]) + self.bus_protocols.append(protocol) + yield from future + if group_monitor_mode: + LOGGER.debug('Starting group monitor') + else: + LOGGER.debug('Starting bus monitor') + + @asyncio.coroutine + def search(self, search_timeout=5, iface=None): + self.iface = iface + self.search_timeout = search_timeout + LOGGER.info('Make sure there are no filtering rules that drop UDP multicast packets!') + yield from self.search_gateways() + for t in self.knx_gateways: + print_knx_target(t) + LOGGER.info('Searching done') + + @asyncio.coroutine + def brute(self, targets=None, bus_target=None): + if targets: + self.set_targets(targets) + tasks = [asyncio.Task(self.bruteforce_auth_key(t, bus_target), loop=self.loop) for t in self.targets] + yield from asyncio.wait(tasks) + + @asyncio.coroutine + def scan(self, targets=None, desc_timeout=2, desc_retries=2, + bus_targets=None, bus_info=False, auth_key=0xffffffff): + """The function that will be called by run_until_complete(). This is the main coroutine.""" + self.auth_key = auth_key + if targets: + self.set_targets(targets) + + self.desc_timeout = desc_timeout + self.desc_retries = desc_retries + workers = [asyncio.Task(self.knx_description_worker(), loop=self.loop) + for _ in range(self.max_workers if len(self.targets) > self.max_workers else len(self.targets))] + self.t0 = time.time() + yield from self.q.join() + self.t1 = time.time() + for w in workers: + w.cancel() + + if bus_targets and self.knx_gateways: + self.bus_info = bus_info + bus_scanners = [asyncio.Task(self.bus_scan(g, bus_targets), loop=self.loop) for g in self.knx_gateways] + yield from asyncio.wait(bus_scanners) + else: + LOGGER.info('Scan took {} seconds'.format(self.t1 - self.t0)) + + for t in self.knx_gateways: + print_knx_target(t) + + @asyncio.coroutine + def group_writer(self, target, value=0, routing=False, desc_timeout=2, + desc_retries=2, iface=False): + self.desc_timeout = desc_timeout + self.desc_retries = desc_retries + self.iface = iface + workers = [asyncio.Task(self.knx_description_worker(), loop=self.loop) + for _ in range(self.max_workers if len(self.targets) > self.max_workers else len(self.targets))] + self.t0 = time.time() + yield from self.q.join() + self.t1 = time.time() + for w in workers: + w.cancel() + + if self.knx_gateways: + # TODO: make sure only a single gateway is supplied + knx_gateway = self.knx_gateways[0] + else: + LOGGER.error('No valid KNX gateway found') + return + + if routing: + # Use KNX Routing to write group values + if 'KNXnet/IP Routing' not in knx_gateway.supported_services: + LOGGER.error('KNX gateway {gateway} does not support Routing'.format( + gateway=knx_gateway.host)) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, struct.pack('256s', str.encode(self.iface))) + + # TODO: what if we have devices that access more advanced payloads? + if isinstance(value, str): + value = int(value) + protocol = KnxRoutingConnection(target=target, value=value) + waiter = asyncio.Future(loop=self.loop) + transport = self.loop._make_datagram_transport( + sock, protocol, ('224.0.23.12', 3671), waiter) + + try: + # Wait until connection_made() has been called on the transport + yield from waiter + except: + LOGGER.error('Creating multicast transport failed!') + transport.close() + return + + else: + # Use KNX Tunnelling to write group values + if 'KNXnet/IP Tunnelling' not in knx_gateway.supported_services: + LOGGER.error('KNX gateway {gateway} does not support Routing'.format( + gateway=knx_gateway.host)) + + future = asyncio.Future() + transport, protocol = yield from self.loop.create_datagram_endpoint( + functools.partial(KnxTunnelConnection, future), + remote_addr=(knx_gateway.host, knx_gateway.port)) + self.bus_protocols.append(protocol) + + # Make sure the tunnel has been established + connected = yield from future + + if connected: + # TODO: what if we have devices that access more advanced payloads? + if isinstance(value, str): + value = int(value) + yield from protocol.apci_group_value_write(target, value=value) + protocol.knx_tunnel_disconnect() diff --git a/libknxmap/data/__init__.py b/libknxmap/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libknx/core.py b/libknxmap/data/constants.py similarity index 52% rename from libknx/core.py rename to libknxmap/data/constants.py index 2ed9b7e..dc8ac0a 100644 --- a/libknx/core.py +++ b/libknxmap/data/constants.py @@ -1,21 +1,31 @@ """General core functionality that is needed for other modules, including constants or package exceptions. This should only be imported by other modules in this package.""" +import collections + __all__ = ['KNX_CONSTANTS', 'KNX_SERVICES', 'KNX_MEDIUMS', + 'KNX_BUS_MEDIUMS', + 'DEVICE_TYPES', + 'LAYER_TYPES', + '_LAYER_TYPES', + 'TPCI_UNNUMBERED_CONTROL_DATA_TYPES', + 'TPCI_NUMBERED_CONTROL_DATA_TYPES', 'KNX_MESSAGE_TYPES', + '_KNX_MESSAGE_TYPES', 'KNX_STATUS_CODES', 'CEMI_PRIMITIVES', - 'CEMI_MESSAGE_CODES', # TODO: maybe find a better solution instead of having the same dict twice - 'TPCI_TYPES', - 'APCI_TYPES', - '_APCI_TYPES', + 'CEMI_MSG_CODES', # TODO: maybe find a better solution instead of having the same dict twice + '_CEMI_MSG_CODES', + 'CEMI_TPCI_TYPES', + '_CEMI_TPCI_TYPES', + 'CEMI_APCI_TYPES', + '_CEMI_APCI_TYPES', + 'OBJECT_TYPES', 'DEVICE_OBJECTS', 'PARAMETER_OBJECTS', - 'DEVICE_DESCRIPTORS', - 'Error'] - + 'OBJECTS'] KNX_CONSTANTS = { 'KNXNETIP_VERSION_10': 0x10, @@ -23,7 +33,6 @@ 'DEFAULT_PORT': 3671, 'MULTICAST_ADDR': '224.0.23.12'} - KNX_SERVICES = { 0x02: 'KNXnet/IP Core', 0x03: 'KNXnet/IP Device Management', @@ -33,7 +42,6 @@ 0x07: 'KNXnet/IP Remote Configuration and Diagnosis', 0x08: 'KNXnet/IP Object Server'} - KNX_MEDIUMS = { 0x01: 'reserved', 0x02: 'KNX TP', @@ -42,6 +50,46 @@ 0x10: 'KNX RF', 0x20: 'KNX IP'} +KNX_BUS_MEDIUMS = { + 0: 'TP1', + 1: 'PL110', + 2: 'RF', + 5: 'KNXnet/IP'} + +DEVICE_TYPES = { + 0x01: 'System 1 (BCU1)', + 0x02: 'System 2 (BCU2)', + 0x70: 'System 7 (BIM M 112)', + 0x7b: 'System B', + 0x30: 'LTE', + 0x91: 'TP1 Line/area coupler - Repeater', + 0x90: 'Media coupler TP1-PL110'} + +LAYER_TYPES = { + 0x02: 'TUNNEL_LINKLAYER', + 0x03: 'DEVICE_MGMT_CONNECTION', + 0x04: 'TUNNEL_RAW', + 0x06: 'REMLOG_CONNECTION', + 0x07: 'REMCONF_CONNECTION', + 0x08: 'OBJSVR_CONNECTION', + 0x80: 'TUNNEL_BUSMONITOR'} + +_LAYER_TYPES = { + 'TUNNEL_LINKLAYER': 0x02, + 'DEVICE_MGMT_CONNECTION': 0x03, + 'TUNNEL_RAW': 0x04, + 'REMLOG_CONNECTION': 0x06, + 'REMCONF_CONNECTION': 0x07, + 'OBJSVR_CONNECTION': 0x08, + 'TUNNEL_BUSMONITOR': 0x80} + +TPCI_UNNUMBERED_CONTROL_DATA_TYPES = { + 'CONNECT': 0x00, + 'DISCONNECT': 0x01} + +TPCI_NUMBERED_CONTROL_DATA_TYPES = { + 'ACK': 0x02, + 'NACK': 0x03} _KNX_MESSAGE_TYPES = { # KNXnet/IP Core @@ -73,7 +121,6 @@ # KNXnet/IP ObjectServer 0x0800: 'OBJECTSERVER_REQUEST'} - KNX_MESSAGE_TYPES = { # KNXnet/IP Core 'SEARCH_REQUEST': 0x0201, @@ -104,16 +151,15 @@ # KNXnet/IP ObjectServer 'OBJECTSERVER_REQUEST': 0x0800} - KNX_STATUS_CODES = { 0x00: 'E_NO_ERROR', 0x01: 'E_HOST_PROTOCOL_TYPE', 0x02: 'E_VERSION_NOT_SUPPORTED', 0x04: 'E_SEQUENCE_NUMBER', # CONNECT_RESPONSE status codes - 0x22: 'E_CONNECTION_TYPE', # requested connection type not supported - 0x23: 'E_CONNECTION_OPTION', # one or more connection options not supported - 0x24: 'E_NO_MORE_CONNECTIONS', # max amount of connections reached, + 0x22: 'E_CONNECTION_TYPE', # requested connection type not supported + 0x23: 'E_CONNECTION_OPTION', # one or more connection options not supported + 0x24: 'E_NO_MORE_CONNECTIONS', # max amount of connections reached, # CONNECTIONSTATE_RESPONSE status codes 0x21: 'E_CONNECTION_ID', 0x26: 'E_DATA_CONNECTION', @@ -121,35 +167,48 @@ # CONNECT_ACK status codes 0x29: 'E_TUNNELLING_LAYER'} - # See: http://www.openremote.org/display/knowledge/Common+External+Message+Interface+(cEMI) CEMI_PRIMITIVES = { 0x10: 'L_Raw.req', - 0x11: 'L_Data.req', # Request + 0x11: 'L_Data.req', # Request 0x13: 'L_Poll_Data.req', 0x25: 'L_Poll_Data.con', - 0x2e: 'L_Data.con', # Confirmation - 0x29: 'L_Data.ind', # Receive a data frame + 0x2e: 'L_Data.con', # Confirmation + 0x29: 'L_Data.ind', # Receive a data frame 0x2b: 'L_Busmon.ind', 0x2d: 'L_Raw.ind', 0x2f: 'L_Raw.con', 0xfb: 'M_PropRead.con', 0xfc: 'M_PropRead.req'} +#: 'M_FuncPropCommand.req', +#: 'M_FuncPropStateRead.req'} -CEMI_MESSAGE_CODES = { +CEMI_MSG_CODES = { 'L_Raw.req': 0x10, - 'L_Data.req': 0x11, # Request + 'L_Data.req': 0x11, # Request 'L_Poll_Data.req': 0x13, 'L_Poll_Data.con': 0x25, - 'L_Data.con': 0x2e, # Confirmation - 'L_Data.ind': 0x29, # Receive a data frame + 'L_Data.con': 0x2e, # Confirmation + 'L_Data.ind': 0x29, # Receive a data frame 'L_Busmon.ind': 0x2b, 'L_Raw.ind': 0x2d, 'L_Raw.con': 0x2f, 'M_PropRead.con': 0xfb, 'M_PropRead.req': 0xfc} +_CEMI_MSG_CODES = { + 0x10: 'L_Raw.req', + 0x11: 'L_Data.req', # Request + 0x13: 'L_Poll_Data.req', + 0x25: 'L_Poll_Data.con', + 0x2e: 'L_Data.con', # Confirmation + 0x29: 'L_Data.ind', # Receive a data frame + 0x2b: 'L_Busmon.ind', + 0x2d: 'L_Raw.ind', + 0x2f: 'L_Raw.con', + 0xfb: 'M_PropRead.con', + 0xfc: 'M_PropRead.req'} CEMI_PRIORITIES = { 0x00: 'system', @@ -157,22 +216,25 @@ 0x02: 'urgent', 0x03: 'low'} - COMM_TYPES = { 0x00: 'Unnumbered Data Packet (UDP)', 0x01: 'Numbered Data Packet (NDP)', 0x02: 'Unnumbered Control Data (UCD)', 0x03: 'Numbered Control Data (NCD)'} - -TPCI_TYPES = { +CEMI_TPCI_TYPES = { 'UDP': 0x00, 'NDP': 0x01, 'UCD': 0x02, 'NCD': 0x03} +_CEMI_TPCI_TYPES = { + 0x00: 'UDP', + 0x01: 'NDP', + 0x02: 'UCD', + 0x03: 'NCD'} -_APCI_TYPES = { +_CEMI_APCI_TYPES = { 0x000: 'A_GroupValue_Read', 0x001: 'A_GroupValue_Response', 0x100: 'A_IndividualAddress_Read', @@ -229,8 +291,7 @@ 0x006: 'A_ADC_Read', 0x0c0: 'A_IndividualAddress_Write'} - -APCI_TYPES = { +CEMI_APCI_TYPES = { 'A_ADC_Read': 0x6, 'A_ADC_Response': 0x1c0, 'A_Authorize_Request': 0x3d1, @@ -287,19 +348,54 @@ 'A_UserMemory_Response': 0x2c1, 'A_UserMemory_Write': 0x2c2} +OBJECT_TYPES = { + 0: 'DEVICE_OBJECT', + 1: 'ADDRESSTABLE_OBJECT', + 2: 'ASSOCIATIONTABLE_OBJECT', + 3: 'APPLICATIONPROGRAM_OBJECT', + 4: 'INTERFACEPROGRAM_OBJECT', + 5: 'EIB_ASSOC_TABLE', + 6: 'ROUTER', + 7: 'LTE_FILTER_TABLE', + 8: 'CEMI_SERVER', + 9: 'GROUP_OBJECT_TABLE', + 10: 'POLLING_MASTER', + 11: 'KNXNET_IP_PARAMETER', + 12: 'APPLICATION_CONTROLLER', + 13: 'FILE_SERVER'} DEVICE_OBJECTS = { + # global object 'PID_OBJECT_TYPE': 0x1, + 'PID_OBJECT_NAME': 0x02, + 'PID_SEMAPHOR': 0x03, + 'PID_GROUP_OBJECT_REFERENCE': 0x04, + 'PID_LOAD_STATE_CONTROL': 0x05, + 'PID_RUN_STATE_CONTROL': 0x06, + 'PID_TABLE_REFERENCE': 0x07, 'PID_SERVICE_CONTROL': 0x8, 'PID_FIRMWARE_REVISION': 0x9, + 'PID_SERVICES_SUPPORTED': 0xa, 'PID_SERIAL_NUMBER': 0xb, 'PID_MANUFACTURER_ID': 0xc, + 'PID_PROGRAM_VERSION': 0xd, 'PID_DEVICE_CONTROL': 0xe, + 'PID_ORDER_INFO': 0xf, + 'PID_PEI_TYPE': 0x10, + 'PID_PORT_CONFIGURATION': 0x11, + 'PID_POLL_GROUP_SETTINGS': 0x12, 'PID_MANUFACTURE_DATA': 0x13, + 'PID_ENABLE': 0x14, + 'PID_DESCRIPTION': 0x15, + 'PID_FILE': 0x16, + 'PID_TABLE': 0x17, + 'PID_GROUP_OBJECT_LINK': 0x1a, + # object 0 'PID_ROUTING_COUNT': 0x33, 'PID_MAX_RETRY_COUNT ': 0x34, 'PID_ERROR_FLAGS': 0x35, 'PID_PROGMODE': 0x36, + 'PID_PRODUCT_ID': 0x37, 'PID_MAX_APDULENGTH': 0x38, 'PID_DEVICE_ADDR': 0x3a, 'PID_PB_CONFIG': 0x3b, @@ -316,8 +412,8 @@ 'PID_DOMAIN_ADDR': 0x46, 'PID_IO_LIST': 0x47} - PARAMETER_OBJECTS = { + # object 11 'PID_PROJECT_INSTALLATION_ID': 0x33, 'PID_KNX_INDIVIDUAL_ADDRESS': 0x34, 'PID_ADDITIONAL_INDIVIDUAL_ADDRESSES': 0x35, @@ -346,40 +442,165 @@ 'PID_FRIENDLY_NAME': 0x4c, 'PID_ROUTING_BUSY_WAIT_TIME': 0x4e} - -DEVICE_DESCRIPTORS = { - 0x0010: 'System 1 (BCU 1)', - 0x0011: 'System 1 (BCU 1)', - 0x0012: 'System 1 (BCU 1)', - 0x0013: 'System 1 (BCU 1)', - 0x0020: 'System 2 (BCU 2)', - 0x0021: 'System 2 (BCU 2)', - 0x0025: 'System 2 (BCU 2)', - 0x0300: 'System 300', - 0x0310: 'TP1 USB interface v1', - 0x0700: 'BIM M112', - 0x0701: 'BIM M112', - 0x0705: 'BIM M112', - 0x07B0: 'System B', - 0x0810: 'IR-Decoder', - 0x0811: 'IR-Decoder', - 0x0910: 'Coupler 1.0', - 0x0911: 'Coupler 1.1', - 0x0912: 'Coupler 1.2', - 0x091A: 'KNXnet/IP Router', - 0x0AFD: 'none', - 0x0AFE: 'none', - 0x1012: 'BCU 1', - 0x1013: 'BCU 1', - 0x17B0: 'System B', - 0x1900: 'Media Coupler PL-TP', - 0x2010: 'Bidirectional devices', - 0x2110: 'Unidirectional devices', - 0x2322: 'RF USB interface v2', - 0x3012: 'BCU 1', - 0x4012: 'BCU 1', - 0x5705: 'System 7'} - - -class Error(Exception): - pass \ No newline at end of file +OBJECTS = { + 0: {'PID_OBJECT_TYPE': 1, + 'PID_OBJECT_NAME': 2, + 'PID_SEMAPHOR': 3, + 'PID_GROUP_OBJECT_REFERENCE': 4, + 'PID_LOAD_STATE_CONTROL': 5, + 'PID_RUN_STATE_CONTROL': 6, + 'PID_TABLE_REFERENCE': 7, + 'PID_SERVICE_CONTROL': 8, + 'PID_FIRMWARE_REVISION': 9, + 'PID_SERVICES_SUPPORTED': 10, + 'PID_SERIAL_NUMBER': 11, + 'PID_MANUFACTURER_ID': 12, + 'PID_PROGRAM_VERSION': 13, + 'PID_DEVICE_CONTROL': 14, + 'PID_ORDER_INFO': 15, + 'PID_PEI_TYPE': 16, + 'PID_PORT_CONFIGURATION': 17, + 'PID_POLL_GROUP_SETTINGS': 18, + 'PID_MANUFACTURER_DATA': 19, + 'PID_ENABLE': 20, + 'PID_DESCRIPTION': 21, + 'PID_FILE': 22, + 'PID_TABLE': 23, + 'PID_ENROL': 24, + 'PID_VERSION': 25, + 'PID_GROUP_OBJECT_LINK': 26, + 'PID_MCB_TABLE': 27, + 'PID_ERROR_CODE': 28, + 'PID_OBJECT_INDEX': 29, + 'PID_ROUTING_COUNT': 51, + 'PID_MAX_RETRY_COUNT': 52, + 'PID_ERROR_FLAGS': 53, + 'PID_PROGMODE': 54, + 'PID_PRODUCT_ID': 55, + 'PID_MAX_APDULENGTH': 56, + 'PID_SUBNET_ADDR': 57, + 'PID_DEVICE_ADDR': 58, + 'PID_PB_CONFIG': 59, + 'PID_ADDR_REPORT': 60, + 'PID_ADDR_CHECK': 61, + 'PID_OBJECT_VALUE': 62, + 'PID_OBJECTLINK': 63, + 'PID_APPLICATION': 64, + 'PID_PARAMETER': 65, + 'PID_OBJECTADDRESS': 66, + 'PID_PSU_TYPE': 67, + 'PID_PSU_STATUS': 68, + 'PID_PSU_ENABLE': 69, + 'PID_DOMAIN_ADDRESS': 70, + 'PID_IO_LIST': 71, + 'PID_MGT_DESCRIPTOR_01': 72, + 'PID_PL110_PARAM': 73, + 'PID_RF_REPEAT_COUNTER': 74, + 'PID_RECEIVE_BLOCK_TABLE': 75, + 'PID_RANDOM_PAUSE_TABLE': 76, + 'PID_RECEIVE_BLOCK_NR': 77, + 'PID_HARDWARE_TYPE': 78, + 'PID_RETRANSMITTER_NUMBER': 79, + 'PID_SERIAL_NR_TABLE': 80, + 'PID_BIBATMASTER_ADDRESS': 81, + 'PID_RF_DOMAIN_ADDRESS': 82, + 'PID_DEVICE_DESCRIPTOR': 83, + 'PID_METERING_FILTER_TABLE': 84, + 'PID_GROUP_TELEGR_RATE_LIMIT_TIME_BASE': 85, + 'PID_GROUP_TELEGR_RATE_LIMIT_NO_OF_TELEGR': 86, + 'PID_CHANNEL_01_PARAM': 101, + 'PID_CHANNEL_02_PARAM': 102, + 'PID_CHANNEL_03_PARAM': 103, + 'PID_CHANNEL_04_PARAM': 104, + 'PID_CHANNEL_05_PARAM': 105, + 'PID_CHANNEL_06_PARAM': 106, + 'PID_CHANNEL_07_PARAM': 107, + 'PID_CHANNEL_08_PARAM': 108, + 'PID_CHANNEL_09_PARAM': 109, + 'PID_CHANNEL_10_PARAM': 110, + 'PID_CHANNEL_11_PARAM': 111, + 'PID_CHANNEL_12_PARAM': 112, + 'PID_CHANNEL_13_PARAM': 113, + 'PID_CHANNEL_14_PARAM': 114, + 'PID_CHANNEL_15_PARAM': 115, + 'PID_CHANNEL_16_PARAM': 116, + 'PID_CHANNEL_17_PARAM': 117, + 'PID_CHANNEL_18_PARAM': 118, + 'PID_CHANNEL_19_PARAM': 119, + 'PID_CHANNEL_20_PARAM': 120, + 'PID_CHANNEL_21_PARAM': 121, + 'PID_CHANNEL_22_PARAM': 122, + 'PID_CHANNEL_23_PARAM': 123, + 'PID_CHANNEL_24_PARAM': 124, + 'PID_CHANNEL_25_PARAM': 125, + 'PID_CHANNEL_26_PARAM': 126, + 'PID_CHANNEL_27_PARAM': 127, + 'PID_CHANNEL_28_PARAM': 128, + 'PID_CHANNEL_29_PARAM': 129, + 'PID_CHANNEL_30_PARAM': 130, + 'PID_CHANNEL_31_PARAM': 131, + 'PID_CHANNEL_32_PARAM': 132}, + 1: {'PID_EXT_FRAMEFORMAT': 51, + 'PID_ADDRTAB1': 52, + 'PID_GROUP_RESPONSER_TABLE': 53}, + 2: {'PID_TABLE': 52}, + 3: {'PID_PARAM_REFERENCE': 51}, + 6: {'PID_LINE_STATUS': 51, + 'PID_MAIN_LCCONFIG': 52, + 'PID_SUB_LCCONFIG': 53, + 'PID_MAIN_LCGRPCONFIG': 54, + 'PID_SUB_LCGRPCONFIG': 55, + 'PID_ROUTETABLE_CONTROL': 56, + 'PID_COUPL_SERV_CONTROL': 57, + 'PID_MAX_APDU_LENGTH': 58}, + 7: {'PID_LTE_ROUTESELECT': 51, + 'PID_LTE_ROUTETABLE': 52}, + 8: {'PID_ADD_INFO_TYPES': 54, + 'PID_TIME_BASE': 55, + 'PID_TRANSP_ENABLE': 56, + 'PID_CLIENT_SNA': 57, + 'PID_CLIENT_DEVICE_ADDRESS': 58, + 'PID_BIBAT_NEXTBLOCK': 59, + 'PID_MEDIUM_TYPE': 51, + 'PID_COMM_MODE': 52, + 'PID_MEDIUM_AVAILABILITY': 53, + 'PID_RF_MODE_SELECT': 60, + 'PID_RF_MODE_SUPPORT': 61, + 'PID_RF_FILTERING_MODE_SELECT': 62, + 'PID_RF_FILTERING_MODE_SUPPORT': 63}, + 9: {'PID_GRPOBJTABLE': 51, + 'PID_EXT_GRPOBJREFERENCE': 52}, + 10: {'PID_POLLING_STATE': 51, + 'PID_POLLING_SLAVE_ADDR': 52, + 'PID_POLL_CYCLE': 53}, + 11: {'PID_PROJECT_INSTALLATION_ID': 51, + 'PID_KNX_INDIVIDUAL_ADDRESS': 52, + 'PID_ADDITIONAL_INDIVIDUAL_ADDRESSES': 53, + 'PID_CURRENT_IP_ASSIGNMENT_METHOD': 54, + 'PID_IP_ASSIGNMENT_METHOD': 55, + 'PID_IP_CAPABILITIES': 56, + 'PID_CURRENT_IP_ADDRESS': 57, + 'PID_CURRENT_SUBNET_MASK': 58, + 'PID_CURRENT_DEFAULT_GATEWAY': 59, + 'PID_IP_ADDRESS': 60, + 'PID_SUBNET_MASK': 61, + 'PID_DEFAULT_GATEWAY': 62, + 'PID_DHCP_BOOTP_SERVER': 63, + 'PID_MAC_ADDRESS': 64, + 'PID_SYSTEM_SETUP_MULTICAST_ADDRESS': 65, + 'PID_ROUTING_MULTICAST_ADDRESS': 66, + 'PID_TTL': 67, + 'PID_KNXNETIP_DEVICE_CAPABILITIES': 68, + 'PID_KNXNETIP_DEVICE_STATE': 69, + 'PID_KNXNETIP_ROUTING_CAPABILITIES': 70, + 'PID_PRIORITY_FIFO_ENABLED': 71, + 'PID_QUEUE_OVERFLOW_TO_IP': 72, + 'PID_QUEUE_OVERFLOW_TO_KNX': 73, + 'PID_MSG_TRANSMIT_TO_IP': 74, + 'PID_MSG_TRANSMIT_TO_KNX': 75, + 'PID_FRIENDLY_NAME': 76}, + 12: {'PID_AR_TYPE_REPORT': 51}} + +for k, v in OBJECTS.items(): + OBJECTS[k] = collections.OrderedDict( + sorted(v.items(), key=lambda v: v[1])) diff --git a/libknx/manufacturers.json b/libknxmap/data/manufacturers.json similarity index 100% rename from libknx/manufacturers.json rename to libknxmap/data/manufacturers.json diff --git a/libknx/gateway.py b/libknxmap/gateway.py similarity index 63% rename from libknx/gateway.py rename to libknxmap/gateway.py index 37c7763..e8f8f9d 100644 --- a/libknx/gateway.py +++ b/libknxmap/gateway.py @@ -2,18 +2,20 @@ import asyncio import logging -from .core import * -from .messages import * +from libknxmap.data.constants import * +from libknxmap.messages import * __all__ = ['KnxGatewaySearch', 'KnxGatewayDescription'] LOGGER = logging.getLogger(__name__) + class KnxGatewaySearch(asyncio.DatagramProtocol): """A protocol implementation for searching KNXnet/IP gateways via multicast messages. The protocol will hold a set responses with all the KNXnet/IP gateway responses.""" + def __init__(self, loop=None): self.loop = loop or asyncio.get_event_loop() self.transport = None @@ -25,28 +27,22 @@ def connection_made(self, transport): self.sockname = self.transport.get_extra_info('sockname') packet = KnxSearchRequest(sockname=self.sockname) self.transport.get_extra_info('socket').sendto(packet.get_message(), - (KNX_CONSTANTS.get('MULTICAST_ADDR'), - KNX_CONSTANTS.get('DEFAULT_PORT'))) + (KNX_CONSTANTS.get('MULTICAST_ADDR'), + KNX_CONSTANTS.get('DEFAULT_PORT'))) def datagram_received(self, data, addr): - try: - LOGGER.debug('Parsing KnxSearchResponse') - response = KnxSearchResponse(data) - if response: - self.responses.add((addr, response)) - else: - LOGGER.debug('Not a valid search response!') - except Exception as e: - LOGGER.exception(e) + knx_message = parse_message(data) + if knx_message and isinstance(knx_message, KnxSearchResponse): + self.responses.add((addr, knx_message)) class KnxGatewayDescription(asyncio.DatagramProtocol): """Protocol implelemtation for KNXnet/IP description requests.""" + def __init__(self, future, loop=None, timeout=2): self.future = future self.loop = loop or asyncio.get_event_loop() self.transport = None - self.response = None self.timeout = timeout def connection_made(self, transport): @@ -58,27 +54,14 @@ def connection_made(self, transport): self.transport.sendto(packet.get_message()) def connection_timeout(self): - LOGGER.debug('Description timeout') self.transport.close() self.future.set_result(False) def datagram_received(self, data, addr): self.wait.cancel() self.transport.close() - try: - LOGGER.debug('Parsing KnxDescriptionResponse') - self.response = KnxDescriptionResponse(data) - if self.response: - self.future.set_result(self.response) - else: - LOGGER.debug('Not a valid description response!') - self.future.set_result(False) - except Exception as e: - LOGGER.exception(e) - - -class KnxDeviceConfigurationConnection(asyncio.DatagramProtocol): - # TODO: implement device configuration connection - - def __init__(self): - pass \ No newline at end of file + knx_message = parse_message(data) + if knx_message and isinstance(knx_message, KnxDescriptionResponse): + self.future.set_result(knx_message) + else: + self.future.set_result(False) diff --git a/libknxmap/manufacturers.py b/libknxmap/manufacturers.py new file mode 100644 index 0000000..104b43d --- /dev/null +++ b/libknxmap/manufacturers.py @@ -0,0 +1,9 @@ +import json + + +def get_manufacturer_by_id(mid): + assert isinstance(mid, int) + m = json.load(open('libknxmap/data/manufacturers.json')) + for _m in m.get('manufacturers'): + if int(_m.get('knx_manufacturer_id')) == mid: + return _m.get('name') diff --git a/libknx/messages.py b/libknxmap/messages.py similarity index 67% rename from libknx/messages.py rename to libknxmap/messages.py index dca94b6..7dcf962 100644 --- a/libknx/messages.py +++ b/libknxmap/messages.py @@ -5,7 +5,7 @@ import socket import struct -from .core import * +from libknxmap.data.constants import * __all__ = ['parse_message', 'KnxMessage', @@ -22,10 +22,14 @@ 'KnxDisconnectRequest', 'KnxDisconnectResponse', 'KnxDeviceConfigurationRequest', - 'KnxDeviceConfigurationAck'] + 'KnxDeviceConfigurationAck', + 'KnxRoutingIndication', + 'KnxRoutingLostMessage', + 'KnxRoutingBusy'] LOGGER = logging.getLogger(__name__) + def parse_message(data): """ Determines the message type of data and returns a corresponding class instance. This is a helper @@ -44,10 +48,17 @@ def parse_message(data): LOGGER.exception(e) return - if message_type == KNX_MESSAGE_TYPES.get('CONNECT_RESPONSE'): + if message_type == KNX_MESSAGE_TYPES.get('SEARCH_RESPONSE'): + LOGGER.debug('Parsing KnxSearchResponse') + return KnxSearchResponse(data) + elif message_type == KNX_MESSAGE_TYPES.get('DESCRIPTION_RESPONSE'): + LOGGER.debug('Parsing KnxDescriptionResponse') + return KnxDescriptionResponse(data) + elif message_type == KNX_MESSAGE_TYPES.get('CONNECT_RESPONSE'): LOGGER.debug('Parsing KnxConnectResponse') return KnxConnectResponse(data) elif message_type == KNX_MESSAGE_TYPES.get('TUNNELLING_REQUEST'): + LOGGER.debug('Parsing KnxTunnellingRequest') return KnxTunnellingRequest(data) elif message_type == KNX_MESSAGE_TYPES.get('TUNNELLING_ACK'): LOGGER.debug('Parsing KnxTunnelingAck') @@ -89,6 +100,7 @@ def __init__(self): self.port = None self.knx_source = None self.knx_destination = None + self.cemi_message_code = None @staticmethod def parse_knx_address(address): @@ -100,7 +112,7 @@ def parse_knx_address(address): -------------------- 4 Bit|4 Bit| 8 Bit - >>> parse_knx_address(99999) + parse_knx_address(99999) '8.6.159' """ assert isinstance(address, int) @@ -110,7 +122,7 @@ def parse_knx_address(address): def pack_knx_address(address): """Pack physical/individual KNX address. - >>> pack_knx_address('15.15.255') + pack_knx_address('15.15.255') 65535 """ assert isinstance(address, str) @@ -121,7 +133,7 @@ def pack_knx_address(address): def parse_knx_group_address(address): """Parse KNX group address. - >>> parse_knx_group_address(12345) + parse_knx_group_address(12345) '6/0/57' """ assert isinstance(address, int) @@ -131,7 +143,7 @@ def parse_knx_group_address(address): def pack_knx_group_address(address): """Pack KNX group address. - >>> pack_knx_group_address('6/0/57') + pack_knx_group_address('6/0/57') 12345 """ assert isinstance(address, str) @@ -142,7 +154,7 @@ def pack_knx_group_address(address): def parse_knx_device_serial(address): """Parse a KNX device serial to human readable format. - >>> parse_knx_device_serial(b'\x00\x00\x00\x00\X12\x23') + parse_knx_device_serial(b'\x00\x00\x00\x00\X12\x23') '000000005C58' """ assert isinstance(address, bytes) @@ -152,21 +164,35 @@ def parse_knx_device_serial(address): def parse_mac_address(address): """Parse a MAC address to human readable format. - >>> parse_mac_address(b'\x12\x34\x56\x78\x90\x12') + parse_mac_address(b'\x12\x34\x56\x78\x90\x12') '12:34:56:78:90:12' """ assert isinstance(address, bytes) return '{0:02X}:{1:02X}:{2:02X}:{3:02X}:{4:02X}:{5:02X}'.format(*address) + @staticmethod + def parse_device_descriptor(desc): + """Parse device descriptors to three separate integers. + + parse_device_descriptor(1793) + (0, 112, 1) + """ + assert isinstance(desc, int), 'Device descriptor is not an int' + desc = format(desc, '04x') + medium = int(desc[0]) + dev_type = int(desc[1:-1], 16) + version = int(desc[-1]) + return medium, dev_type, version + def set_peer(self, peer): - assert isinstance(peer, tuple), ('Peer is not a tuple') + assert isinstance(peer, tuple), 'Peer is not a tuple' self.source, self.port = peer def set_source_ip(self, address): self.source = address def set_source_port(self, port): - assert isinstance(port, int), ('Port is not an int') + assert isinstance(port, int), 'Port is not an int' self.port = port def set_knx_source(self, address): @@ -175,7 +201,12 @@ def set_knx_source(self, address): def set_knx_destination(self, address): """Set the KNX destination address of a KnxMessage instance.""" - self.knx_destination = self.pack_knx_address(address) + if '.' in address: + self.knx_destination = self.pack_knx_address(address) + elif '/' in address: + self.knx_destination = self.pack_knx_group_address(address) + else: + LOGGER.error('Invalid address') def get_message(self): """Return the current message.""" @@ -187,7 +218,7 @@ def pack_knx_message(self): message_body = self._pack_knx_body() else: message_body = self.body - self.header['total_length'] = 6 + len(message_body) # header size is always 6 + self.header['total_length'] = 6 + len(message_body) # header size is always 6 self.message = self._pack_knx_header() self.message += message_body @@ -216,7 +247,7 @@ def _unpack_knx_header(self, message): except struct.error as e: LOGGER.exception(e) - def _pack_knx_body(self): + def _pack_knx_body(self, *args, **kwargs): """Subclasses must define this method.""" raise NotImplementedError @@ -239,14 +270,14 @@ def _parse_knx_body_hpai(self, message): self.body['hpai']['protocol_code'], \ self.body['hpai']['ip_address'], \ self.body['hpai']['port'] = struct.unpack('!BBHH', message[:8]) - self.body['hpai']['ip_address'] = socket.inet_aton(self.body['hpai']['ip_address']) # most likely not works + self.body['hpai']['ip_address'] = socket.inet_aton(self.body['hpai']['ip_address']) return message[8:] except struct.error as e: LOGGER.exception(e) def _pack_hpai(self): - hpai = struct.pack('!B', 8) # structure_length - hpai += struct.pack('!B', 0x01) # protocol code + hpai = struct.pack('!B', 8) # structure_length + hpai += struct.pack('!B', 0x01) # protocol code hpai += socket.inet_aton(self.source) hpai += struct.pack('!H', self.port) return hpai @@ -264,7 +295,7 @@ def _unpack_dib_dev_info(self, message): dib_dev_info['structure_length'] = self._unpack_stream('!B', message) dib_dev_info['description_type'] = self._unpack_stream('!B', message) dib_dev_info['knx_medium'] = self._unpack_stream('!B', message) - dib_dev_info['device_status'] = 'PROGMODE_ON' if self._unpack_stream('!B', message) else 'PROGMODE_OFF' + dib_dev_info['device_status'] = self.unpack_cemi_runstate(self._unpack_stream('!B', message)) dib_dev_info['knx_address'] = self.parse_knx_address(self._unpack_stream('!H', message)) dib_dev_info['project_install_identifier'] = self._unpack_stream('!H', message) dib_dev_info['knx_device_serial'] = self.parse_knx_device_serial( @@ -279,13 +310,11 @@ def _unpack_dib_supp_sv_families(self, message): dib_supp_sv_families['structure_length'] = self._unpack_stream('!B', message) dib_supp_sv_families['description_type'] = self._unpack_stream('!B', message) dib_supp_sv_families['families'] = {} - for i in range(int((dib_supp_sv_families['structure_length'] - 2) / 2)): service_id = self._unpack_stream('!B', message) version = self._unpack_stream('!B', message) dib_supp_sv_families['families'][service_id] = dict() dib_supp_sv_families['families'][service_id]['version'] = version - return dib_supp_sv_families @staticmethod @@ -325,10 +354,25 @@ def pack_cemi_cf1(confirm=False, acknowledge_req=False, priority=0x00, cf |= priority << 2 cf |= (1 if system_broadcast else 0) << 4 cf |= (1 if repeat_flag else 0) << 5 - cf |= 0 << 6 # reserved + cf |= 0 << 6 # reserved cf |= (1 if frame_type else 0) << 7 return cf + @staticmethod + def unpack_cemi_cf1(data): + """Parse controlfield1 to a drict.""" + cf = dict() + cf['confirm'] = (data >> 0) & 1 + cf['acknowledge_req'] = (data >> 1) & 1 + cf['priority'] = 0 + cf['priority'] |= ((data >> 2) & 1) << 0 + cf['priority'] |= ((data >> 3) & 1) << 1 + cf['system_broadcast'] = (data >> 4) & 1 + cf['repeat_flag'] = (data >> 5) & 1 + cf['reserved'] = (data >> 6) & 1 + cf['frame_type'] = (data >> 7) & 1 + return cf + @staticmethod def pack_cemi_cf2(ext_frame_format=0x00, hop_count=6, address_type=False): """Pack controlfield2 of the cEMI message. @@ -348,21 +392,6 @@ def pack_cemi_cf2(ext_frame_format=0x00, hop_count=6, address_type=False): cf |= (1 if address_type else 0) << 7 return cf - @staticmethod - def unpack_cemi_cf1(data): - """Parse controlfield1 to a drict.""" - cf = dict() - cf['confirm'] = (data >> 0) & 1 - cf['acknowledge_req'] = (data >> 1) & 1 - cf['priority'] = 0 - cf['priority'] |= ((data >> 2) & 1) << 0 - cf['priority'] |= ((data >> 3) & 1) << 1 - cf['system_broadcast'] = (data >> 4) & 1 - cf['repeat_flag'] = (data >> 5) & 1 - cf['reserved'] = (data >> 6) & 1 - cf['frame_type'] = (data >> 7) & 1 - return cf - @staticmethod def unpack_cemi_cf2(data): """Parse controlfield2 to a drict.""" @@ -379,14 +408,75 @@ def unpack_cemi_cf2(data): cf['address_type'] = (data >> 7) & 1 return cf - def _pack_cemi(self, message_code=None): + @staticmethod + def pack_cemi_runstate(prog_mode=False, link_layer_active=False, transport_layer_active=False, + app_layer_active=False, serial_interface_active=False, user_app_run=False, + bcu_download_mode=False, parity=0): + """Pack runstate field of the cEMI message. + + Bit | + ------+--------------------------------------------------------------- + 7 | Parity + | Even parity for bit 0-6 + ------+--------------------------------------------------------------- + 6 | DM + | BCU in download mode + ------+--------------------------------------------------------------- + 5 | UE + | User application running + ------+--------------------------------------------------------------- + 4 | SE + | Serial interface active + ------+--------------------------------------------------------------- + 3 | ALE + | Application layer active + ------+--------------------------------------------------------------- + 2 | TLE + | Transport layer active + ------+--------------------------------------------------------------- + 1 | LLM + | Link layer active + ------+--------------------------------------------------------------- + 0 | PROG + | Device is in programming mode + ------+---------------------------------------------------------------""" + state = 0 + state |= (1 if prog_mode else 0) << 0 + state |= (1 if link_layer_active else 0) << 1 + state |= (1 if transport_layer_active else 0) << 2 + state |= (1 if app_layer_active else 0) << 3 + state |= (1 if serial_interface_active else 0) << 4 + state |= (1 if user_app_run else 0) << 5 + state |= (1 if bcu_download_mode else 0) << 6 + for i in range(7): + parity ^= (state >> i) & 1 + state |= parity << 7 + return state + + @staticmethod + def unpack_cemi_runstate(data): + """Parse runstate field to a drict.""" + state = collections.OrderedDict() + state['PROG_MODE'] = (data >> 0) & 1 + state['LINK_LAYER'] = (data >> 1) & 1 + state['TRANSPORT_LAYER'] = (data >> 2) & 1 + state['APP_LAYER'] = (data >> 3) & 1 + state['SERIAL_INTERFACE'] = (data >> 4) & 1 + state['USER_APP'] = (data >> 5) & 1 + state['BC_DM'] = (data >> 6) & 1 + # We don't really care about the parity + # state['parity'] = (data >> 7) & 1 + return state + + def _pack_cemi(self, message_code=None, *args, **kwargs): message_code = message_code if message_code else self.cemi_message_code - cemi = struct.pack('!B', message_code) # cEMI message code - cemi += struct.pack('!B', 0) # add information length # TODO: implement variable length if additional information is included - cemi += struct.pack('!B', self.pack_cemi_cf1()) # controlfield 1 - cemi += struct.pack('!B', self.pack_cemi_cf2()) # controlfield 2 - cemi += struct.pack('!H', self.knx_source) # source address (KNX address) - cemi += struct.pack('!H', self.knx_destination) # KNX destination address (either group or physical) + cemi = struct.pack('!B', message_code) # cEMI message code + # TODO: implement variable length if additional information is included + cemi += struct.pack('!B', 0) # add information length + cemi += struct.pack('!B', self.pack_cemi_cf1()) # controlfield 1 + cemi += struct.pack('!B', self.pack_cemi_cf2(*args, **kwargs)) # controlfield 2 + cemi += struct.pack('!H', self.knx_source) # source address (KNX address) + cemi += struct.pack('!H', self.knx_destination) # KNX destination address (either group or physical) return cemi def _unpack_cemi(self, message): @@ -403,6 +493,7 @@ def _unpack_cemi(self, message): cemi['additional_information']['extended_relative_timestamp'] = self._unpack_stream('!B', message) cemi['additional_information']['extended_relative_timestamp'] = self._unpack_stream('!I', message) cemi['raw_frame'] = message.read() + return cemi cemi['controlfield_1'] = self.unpack_cemi_cf1(self._unpack_stream('!B', message)) cemi['controlfield_2'] = self.unpack_cemi_cf2(self._unpack_stream('!B', message)) @@ -426,8 +517,8 @@ def _unpack_cemi(self, message): cemi['tpci']['type'] = tpci_unpacked['tpci_type'] cemi['tpci']['sequence'] = tpci_unpacked['sequence'] - if tpci_unpacked['tpci_type'] is 2 or \ - tpci_unpacked['tpci_type'] is 3: + if tpci_unpacked['tpci_type'] is [2, 3]: + # Control data includes a status field tpci_unpacked['status'] = 0 tpci_unpacked['status'] |= ((tpci[0] >> 0) & 1) << 0 tpci_unpacked['status'] |= ((tpci[0] >> 1) & 1) << 1 @@ -440,7 +531,7 @@ def _unpack_cemi(self, message): tpci_unpacked['apci'] |= ((tpci[0] >> 0) & 1) << 2 tpci_unpacked['apci'] |= ((tpci[0] >> 1) & 1) << 3 - if tpci_unpacked['apci'] in APCI_TYPES.values(): + if tpci_unpacked['apci'] in CEMI_APCI_TYPES.values(): tpci_unpacked['apci_data'] = 0 tpci_unpacked['apci_data'] |= ((tpci[1] >> 0) & 1) << 0 tpci_unpacked['apci_data'] |= ((tpci[1] >> 1) & 1) << 1 @@ -449,34 +540,220 @@ def _unpack_cemi(self, message): tpci_unpacked['apci_data'] |= ((tpci[1] >> 4) & 1) << 4 tpci_unpacked['apci_data'] |= ((tpci[1] >> 5) & 1) << 5 else: - tpci_unpacked['apci'] = tpci_unpacked['apci'] << 2 + tpci_unpacked['apci'] <<= 2 tpci_unpacked['apci'] |= ((tpci[1] >> 4) & 1) << 0 tpci_unpacked['apci'] |= ((tpci[1] >> 5) & 1) << 1 - if tpci_unpacked['apci'] in APCI_TYPES.values(): + if tpci_unpacked['apci'] in CEMI_APCI_TYPES.values(): tpci_unpacked['apci_data'] = 0 tpci_unpacked['apci_data'] |= ((tpci[1] >> 0) & 1) << 0 tpci_unpacked['apci_data'] |= ((tpci[1] >> 1) & 1) << 1 tpci_unpacked['apci_data'] |= ((tpci[1] >> 2) & 1) << 2 tpci_unpacked['apci_data'] |= ((tpci[1] >> 3) & 1) << 3 else: - tpci_unpacked['apci'] = tpci_unpacked['apci'] << 4 + tpci_unpacked['apci'] <<= 4 tpci_unpacked['apci'] |= ((tpci[1] >> 0) & 1) << 0 tpci_unpacked['apci'] |= ((tpci[1] >> 1) & 1) << 1 tpci_unpacked['apci'] |= ((tpci[1] >> 2) & 1) << 2 tpci_unpacked['apci'] |= ((tpci[1] >> 3) & 1) << 3 - cemi['apci'] = tpci_unpacked['apci'] - cemi['apci_data'] = tpci_unpacked.get('apci_data') + cemi['apci'] = dict() + cemi['apci']['type'] = tpci_unpacked['apci'] + cemi['apci']['data'] = tpci_unpacked.get('apci_data') cemi['data'] = tpci[2:] # TODO: if there is more data, read it now # TODO: read cemi['npdu_len']-1 bytes return cemi + def tpci_unnumbered_control_data(self, ucd_type): + assert ucd_type in TPCI_UNNUMBERED_CONTROL_DATA_TYPES.keys(), 'Invalid UCD type: {}'.format(ucd_type) + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 0) # Data length + npdu = CEMI_TPCI_TYPES.get('UCD') << 14 + npdu |= TPCI_UNNUMBERED_CONTROL_DATA_TYPES.get(ucd_type) << 8 + cemi += struct.pack('!H', npdu) + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() -class KnxSearchRequest(KnxMessage): + def tpci_numbered_control_data(self, ncd_type, sequence=0): + assert ncd_type in TPCI_NUMBERED_CONTROL_DATA_TYPES.keys(), 'Invalid NCD type: {}'.format(ncd_type) + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 0) # Data length + npdu = CEMI_TPCI_TYPES.get('NCD') << 14 + npdu |= sequence << 10 + npdu |= TPCI_NUMBERED_CONTROL_DATA_TYPES.get(ncd_type) << 8 + cemi += struct.pack('!H', npdu) + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + def apci_device_descriptor_read(self, sequence=0): + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 1) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_DeviceDescriptor_Read'] << 0 + cemi += struct.pack('!H', npdu) + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_individual_address_read(self, sequence=0): + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 1) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_IndividualAddress_Read'] << 0 + cemi += struct.pack('!H', npdu) + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_authorize_request(self, sequence=0, key=0xffffffff): + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 6) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_Authorize_Request'] << 0 + cemi += struct.pack('!H', npdu) + cemi += struct.pack('!B', 0) # reserved + cemi += struct.pack('!I', key) # key + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_property_value_read(self, sequence=0, object_index=0, property_id=0x0f, + num_elements=1, start_index=1): + """A_PropertyValue_Read""" + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 5) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_PropertyValue_Read'] << 0 + cemi += struct.pack('!H', npdu) + cemi += struct.pack('!B', object_index) # object index + cemi += struct.pack('!B', property_id) # property id + count_index = num_elements << 12 + count_index |= start_index << 0 + cemi += struct.pack('!H', count_index) # number of elements + start index + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_property_description_read(self, sequence=0, object_index=0, property_id=0x0f, + num_elements=1, start_index=1): + """A_PropertyDescription_Read""" + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 5) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_PropertyDescription_Read'] << 0 + cemi += struct.pack('!H', npdu) + cemi += struct.pack('!B', object_index) # object index + cemi += struct.pack('!B', property_id) # property id + count_index = num_elements << 12 + count_index |= start_index << 0 + cemi += struct.pack('!H', count_index) # number of elements + start index + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_adc_read(self, sequence=0): + """A_ADC_Read""" + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 2) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_ADC_Read'] << 0 + npdu |= 1 << 0 # channel nr + cemi += struct.pack('!H', npdu) + cemi += struct.pack('!B', 0x08) # data + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_memory_read(self, sequence=0, memory_address=0x0060, read_count=1): + """A_Memory_Read + + 0x0060 -> run state + 0x010d -> run error + + EEPROM: + 0x0100 OptionReg: Option Register (MC68HC05B06) + 0x0101 ManData: Data provided by the manufacturer of the BCU (see further down) (3 Bytes) + 0x0104 Manufact: ID of the application manufacturer + 0x0105 DevTyp: Manufacturer-specific device type ID (2 Bytes) + 0x0107 Version: Version number of the application program + 0x0108 CheckLim: Specifies the end address of the EEPROM range that is to be covered by + the system check procedure. The address area to be checked ranges from + $0108 to $100+ChekLim-1. + 0x0109 PEI type: Type of PEI required for the application program + 0x010A SyncRate: Baud rate for the PEIs of type 12,14 ‘serial synchronous PEI’ + 0x010B PortCDDR: Defines the directions of data flow of port C for a PEI of type 17 ‘ + programmable I/O’ + 0x010C PortADDR: Defines the directions of data flow for port A. + 0x010D RunError: Runtime error flags + Bit | + ------+--------------------------------------------------------------- + 7 | Unknown + | + ------+--------------------------------------------------------------- + 6 | SYS3_ERR (internal system failure) + | Memory control block broken + ------+--------------------------------------------------------------- + 5 | SYS2_ERR (internal system failure) + | Temperature + ------+--------------------------------------------------------------- + 4 | OBJ_ERR + | RAM flag failure + ------+--------------------------------------------------------------- + 3 | STK_OVL + | Stack overload + ------+--------------------------------------------------------------- + 2 | EEPROM_ERR + | EEPROM encountered checksum error + ------+--------------------------------------------------------------- + 1 | SYS1_ERR (internal system failure) + | Wrong parity bit + ------+--------------------------------------------------------------- + 0 | SYS0_ERR (internal system failure) + | Message buffer offset broken + ------+--------------------------------------------------------------- + 0x010E RouteCnt: Routing counter constant (layer 3), structure: + 0ccc0000, ccc = routing counter constant (0 to 7) + 0x010F MxRstCnt: Contains the INAK and BUSY retries (layer 2), structure: + bbb00iii, bbb=BUSY retries + iii=INAK retries + 0x0110 ConfigDes: Configuration descriptor (see further down) + 0x0111 AssocTabPtr: Pointer to the Association Table (layer 7) + 0x0112 CommsTabPtr: Pointer to the Table of group objects + 0x0113 UsrInitPtr: Pointer to the initialization routine of the application program + 0x0114 UsrPrgPtr: Pointer to the application program + 0x0115 UsrSavPtr: Pointer to the SAVE subroutine of the application program + 0x0116 AdrTab: Address table (layers 2 and 4) + m = No. of group addresses (1 + (1 + m) * 2 Bytes) + ...0x01FE Application program UsrPrg, + Initialisation program UsrInit, + SAVE subroutine UsrSav + 0x01FF EE_EXOR: EEPROM checksum for the range to be checked (cp. CheckLim) + """ + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req')) + cemi += struct.pack('!B', 3) # Data length + npdu = CEMI_TPCI_TYPES.get('NDP') << 14 + npdu |= sequence << 10 + npdu |= CEMI_APCI_TYPES['A_Memory_Read'] << 4 + npdu |= read_count << 0 # number of octets to read/write + cemi += struct.pack('!H', npdu) + cemi += struct.pack('!H', memory_address) # memory address + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + def apci_group_value_write(self, value=0): + cemi = self._pack_cemi(message_code=CEMI_MSG_CODES.get('L_Data.req'), address_type=True) + cemi += struct.pack('!B', 1) # Data length + npdu = CEMI_TPCI_TYPES.get('UDP') << 14 + npdu |= CEMI_APCI_TYPES['A_GroupValue_Write'] << 6 + npdu |= value << 0 + cemi += struct.pack('!H', npdu) + self._pack_knx_body(cemi=cemi) + self.pack_knx_message() + + +class KnxSearchRequest(KnxMessage): def __init__(self, message=None, sockname=None): super(KnxSearchRequest, self).__init__() if message: @@ -503,7 +780,6 @@ def _unpack_knx_body(self, message): class KnxSearchResponse(KnxMessage): - def __init__(self, message=None): super(KnxSearchResponse, self).__init__() if message: @@ -526,7 +802,6 @@ def _unpack_knx_body(self, message): class KnxDescriptionRequest(KnxMessage): - def __init__(self, message=None, sockname=None): super(KnxDescriptionRequest, self).__init__() if message: @@ -553,7 +828,6 @@ def _unpack_knx_body(self, message): class KnxDescriptionResponse(KnxMessage): - def __init__(self, message=None): super(KnxDescriptionResponse, self).__init__() if message: @@ -575,24 +849,6 @@ def _unpack_knx_body(self, message): class KnxConnectRequest(KnxMessage): - # TODO: move constants to core.py - layer_types = { - 0x02: 'TUNNEL_LINKLAYER', - 0x03: 'DEVICE_MGMT_CONNECTION', - 0x04: 'TUNNEL_RAW', - 0x06: 'REMLOG_CONNECTION', - 0x07: 'REMCONF_CONNECTION', - 0x08: 'OBJSVR_CONNECTION', - 0x80: 'TUNNEL_BUSMONITOR'} - _layer_types = { - 'TUNNEL_LINKLAYER': 0x02, - 'DEVICE_MGMT_CONNECTION': 0x03, - 'TUNNEL_RAW': 0x04, - 'REMLOG_CONNECTION': 0x06, - 'REMCONF_CONNECTION': 0x07, - 'OBJSVR_CONNECTION': 0x08, - 'TUNNEL_BUSMONITOR': 0x80} - def __init__(self, message=None, sockname=None, layer_type='TUNNEL_LINKLAYER', connection_type=0x04): super(KnxConnectRequest, self).__init__() @@ -601,7 +857,7 @@ def __init__(self, message=None, sockname=None, layer_type='TUNNEL_LINKLAYER', else: self.header['service_type'] = KNX_MESSAGE_TYPES.get('CONNECT_REQUEST') self.connection_type = connection_type - self.layer_type = self._layer_types.get(layer_type) + self.layer_type = _LAYER_TYPES.get(layer_type) try: self.source, self.port = sockname self.pack_knx_message() @@ -619,7 +875,8 @@ def _pack_knx_body(self): self.body += struct.pack('!B', 4) # structure_length else: self.body += struct.pack('!B', 2) # structure_length - self.body += struct.pack('!B', self.connection_type) # connection type # TODO: implement other connections (routing, object server) + # TODO: implement other connections (routing, object server) + self.body += struct.pack('!B', self.connection_type) # connection type if self.connection_type == 0x04: self.body += struct.pack('!B', self.layer_type) # knx layer type self.body += struct.pack('!B', 0x00) # reserved @@ -643,7 +900,6 @@ def _unpack_knx_body(self, message): class KnxConnectResponse(KnxMessage): - def __init__(self, message=None): super(KnxConnectResponse, self).__init__() self.ERROR = None @@ -675,16 +931,15 @@ def _unpack_knx_body(self, message): self.body['data_block']['structure_length'] = self._unpack_stream('!B', message) self.body['data_block']['connection_type'] = self._unpack_stream('!B', message) if self.body['data_block']['connection_type'] == 0x04: - self.body['data_block']['knx_address'] = self.parse_knx_address(self._unpack_stream('!H', message)) + self.body['data_block']['knx_address'] = super().parse_knx_address(self._unpack_stream('!H', message)) except Exception as e: LOGGER.exception(e) class KnxTunnellingRequest(KnxMessage): - def __init__(self, message=None, sockname=None, communication_channel=None, knx_source=None, knx_destination=None, sequence_count=0, message_code=0x11, - cemi_ndpu_len=1): + cemi_ndpu_len=0): super(KnxTunnellingRequest, self).__init__() if message: self.unpack_knx_message(message) @@ -693,7 +948,7 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.communication_channel = communication_channel self.sequence_count = sequence_count self.cemi_message_code = message_code - self.cemi_npdu_len = 0 + self.cemi_npdu_len = cemi_ndpu_len if knx_source: self.set_knx_source(knx_source) if knx_destination: @@ -707,10 +962,10 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.port = None def _pack_knx_body(self, cemi=None): - self.body = struct.pack('!B', 4) # structure_length - self.body += struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', self.sequence_count) # sequence counter - self.body += struct.pack('!B', 0) # reserved + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', self.sequence_count) # sequence counter + self.body += struct.pack('!B', 0) # reserved # cEMI if cemi: self.body += cemi @@ -730,199 +985,8 @@ def _unpack_knx_body(self, message): except Exception as e: LOGGER.exception(e) - def tpci_unnumbered_control_data(self, ucd_type): - TYPES = {'CONNECT': 0x00, - 'DISCONNECT': 0x01} - assert ucd_type in TYPES.keys(), 'Invalid UCD type: {}'.format(ucd_type) - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 0) # Data length - npdu = TPCI_TYPES.get('UCD') << 14 - npdu |= TYPES.get(ucd_type) << 8 - cemi += struct.pack('!H', npdu) - self._pack_knx_body(cemi) - self.pack_knx_message() - - def tpci_numbered_control_data(self, ncd_type, sequence=0): - TYPES = {'ACK': 0x02, - 'NACK': 0x03} - assert ncd_type in TYPES.keys(), 'Invalid NCD type: {}'.format(ncd_type) - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 0) # Data length - npdu = TPCI_TYPES.get('NCD') << 14 - npdu |= sequence << 10 - npdu |= TYPES.get(ncd_type) << 8 - cemi += struct.pack('!H', npdu) - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_device_descriptor_read(self, sequence=0): - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 1) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_DeviceDescriptor_Read'] << 0 - cemi += struct.pack('!H', npdu) - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_individual_address_read(self, sequence=0): - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 1) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_IndividualAddress_Read'] << 0 - cemi += struct.pack('!H', npdu) - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_authorize_request(self, sequence=0, key=0xffffffff): - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 6) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_Authorize_Request'] << 0 - cemi += struct.pack('!H', npdu) - cemi += struct.pack('!B', 0) # reserved - cemi += struct.pack('!I', key) # key - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_property_value_read(self, sequence=0, object_index=0, property_id=0x0f, - num_elements=1, start_index=0): - """A_PropertyValue_Read - - object index: 0x00, property id: 0x0f -> order number - object index: 0x00, property id: 0x0b -> serial number - object index: 0x03, property id: 0x0d -> application programm, ABB A021 v2.0, 0002a02120 - object index: 0x03, property id: 0x06 -> 0x01 - object index: 0x04, property id: 0x0d -> - - object index: 0x04, property id: 0x06 -> - - """ - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 5) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_PropertyValue_Read'] << 0 - cemi += struct.pack('!H', npdu) - cemi += struct.pack('!B', object_index) # object index - cemi += struct.pack('!B', property_id) # property id - count_index = num_elements << 12 - count_index |= start_index << 0 - cemi += struct.pack('!H', count_index) # number of elements + start index - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_property_description_read(self, sequence=0, object_index=0, property_id=0x0f, - num_elements=1, start_index=0): - """A_PropertyDescription_Read""" - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 5) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_PropertyDescription_Read'] << 0 - cemi += struct.pack('!H', npdu) - cemi += struct.pack('!B', object_index) # object index - cemi += struct.pack('!B', property_id) # property id - count_index = num_elements << 12 - count_index |= start_index << 0 - cemi += struct.pack('!H', count_index) # number of elements + start index - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_adc_read(self, sequence=0): - """A_ADC_Read""" - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 2) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_ADC_Read'] << 0 - npdu |= 1 << 0 # channel nr - cemi += struct.pack('!H', npdu) - cemi += struct.pack('!B', 0x08) # data - self._pack_knx_body(cemi) - self.pack_knx_message() - - def apci_memory_read(self, sequence=0, memory_address=0x0060, read_count=1): - """A_Memory_Read - - 0x0060 -> run state - - Bit | - ------+--------------------------------------------------------------- - 7 | Parity - | Even parity for bit 0-6 - ------+--------------------------------------------------------------- - 6 | DM - | BCU in download mode - ------+--------------------------------------------------------------- - 5 | UE - | User application running - ------+--------------------------------------------------------------- - 4 | SE - | Serial interface active - ------+--------------------------------------------------------------- - 3 | ALE - | Application layer active - ------+--------------------------------------------------------------- - 2 | TLE - | Transport layer active - ------+--------------------------------------------------------------- - 1 | LLM - | Link layer active - ------+--------------------------------------------------------------- - 0 | PROG - | Device is in programming mode - ------+--------------------------------------------------------------- - - 0x010d -> run error - - Bit | - ------+--------------------------------------------------------------- - 7 | Unknown - | - ------+--------------------------------------------------------------- - 6 | SYS3_ERR (internal system failure) - | Memory control block broken - ------+--------------------------------------------------------------- - 5 | SYS2_ERR (internal system failure) - | Temperature - ------+--------------------------------------------------------------- - 4 | OBJ_ERR - | RAM flag failure - ------+--------------------------------------------------------------- - 3 | STK_OVL - | Stack overload - ------+--------------------------------------------------------------- - 2 | EEPROM_ERR - | EEPROM encountered checksum error - ------+--------------------------------------------------------------- - 1 | SYS1_ERR (internal system failure) - | Wrong parity bit - ------+--------------------------------------------------------------- - 0 | SYS0_ERR (internal system failure) - | Message buffer offset broken - ------+--------------------------------------------------------------- - - 0x0104 -> manufacturer id - 0xb6ec -> 0x01 - 0xb6ed -> 0x01 - 0xb6ea -> 0x01 - 0xb6eb -> 0x01 - """ - cemi = self._pack_cemi(message_code=CEMI_MESSAGE_CODES.get('L_Data.req')) - cemi += struct.pack('!B', 3) # Data length - npdu = TPCI_TYPES.get('NDP') << 14 - npdu |= sequence << 10 - npdu |= APCI_TYPES['A_Memory_Read'] << 4 - npdu |= read_count << 0 # number of octets to read/write - cemi += struct.pack('!H', npdu) - cemi += struct.pack('!H', memory_address) # memory address - self._pack_knx_body(cemi) - self.pack_knx_message() - class KnxTunnellingAck(KnxMessage): - def __init__(self, message=None, communication_channel=None, sequence_count=0): super(KnxTunnellingAck, self).__init__() if message: @@ -934,10 +998,10 @@ def __init__(self, message=None, communication_channel=None, sequence_count=0): self.pack_knx_message() def _pack_knx_body(self): - self.body = struct.pack('!B', 4) # structure_length - self.body += struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', self.sequence_count) # sequence counter - self.body += struct.pack('!B', 0) # status + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', self.sequence_count) # sequence counter + self.body += struct.pack('!B', 0) # status return self.body def _unpack_knx_body(self, message): @@ -952,9 +1016,7 @@ def _unpack_knx_body(self, message): class KnxConnectionStateRequest(KnxMessage): - - def __init__(self, message=None, sockname=None, communication_channel=None, - knx_source=None, knx_destination=None): + def __init__(self, message=None, sockname=None, communication_channel=None): super(KnxConnectionStateRequest, self).__init__() if message: self.unpack_knx_message(message) @@ -969,8 +1031,8 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.port = None def _pack_knx_body(self): - self.body = struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', 0) # reserved + self.body = struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', 0) # reserved # HPAI self.body += self._pack_hpai() return self.body @@ -987,9 +1049,7 @@ def _unpack_knx_body(self, message): class KnxConnectionStateResponse(KnxMessage): - - def __init__(self, message=None, communication_channel=None, - knx_source=None, knx_destination=None): + def __init__(self, message=None, communication_channel=None): super(KnxConnectionStateResponse, self).__init__() if message: self.unpack_knx_message(message) @@ -1014,9 +1074,7 @@ def _unpack_knx_body(self, message): class KnxDisconnectRequest(KnxMessage): - - def __init__(self, message=None, sockname=None, communication_channel=None, - knx_source=None, knx_destination=None): + def __init__(self, message=None, sockname=None, communication_channel=None): super(KnxDisconnectRequest, self).__init__() if message: self.unpack_knx_message(message) @@ -1031,8 +1089,8 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.port = None def _pack_knx_body(self): - self.body = struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', 0) # reserved + self.body = struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', 0) # reserved # HPAI self.body += self._pack_hpai() return self.body @@ -1049,9 +1107,7 @@ def _unpack_knx_body(self, message): class KnxDisconnectResponse(KnxMessage): - - def __init__(self, message=None, communication_channel=None, - knx_source=None, knx_destination=None): + def __init__(self, message=None, communication_channel=None): super(KnxDisconnectResponse, self).__init__() if message: self.unpack_knx_message(message) @@ -1076,6 +1132,7 @@ def _unpack_knx_body(self, message): class KnxDeviceConfigurationRequest(KnxMessage): + # TODO: properly implement configuration requests def __init__(self, message=None, sockname=None, communication_channel=None, sequence_count=0, message_code=0xfc, cemi_ndpu_len=1): @@ -1087,7 +1144,7 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.communication_channel = communication_channel self.sequence_count = sequence_count self.cemi_message_code = message_code - self.cemi_npdu_len = 0 + self.cemi_npdu_len = cemi_ndpu_len try: self.source, self.port = sockname self.pack_knx_message() @@ -1096,23 +1153,23 @@ def __init__(self, message=None, sockname=None, communication_channel=None, self.port = None def _pack_knx_body(self, cemi=None): - self.body = struct.pack('!B', 4) # structure_length - self.body += struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', self.sequence_count) # sequence counter - self.body += struct.pack('!B', 0) # reserved + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', self.sequence_count) # sequence counter + self.body += struct.pack('!B', 0) # reserved # cEMI - #if cemi: + # if cemi: # self.body += cemi - #else: + # else: # self.body += self._pack_cemi() - self.body += struct.pack('!B', self.cemi_message_code) # M_PropRead.req - #self.body += struct.pack('!B', CEMI_MESSAGE_CODES.get('L_Data.req')) + self.body += struct.pack('!B', self.cemi_message_code) # M_PropRead.req + # self.body += struct.pack('!B', CEMI_MESSAGE_CODES.get('L_Data.req')) self.body += struct.pack('!H', 11) self.body += struct.pack('!B', 11) self.body += struct.pack('!B', PARAMETER_OBJECTS.get('PID_ADDITIONAL_INDIVIDUAL_ADDRESSES')) - #self.body += struct.pack('!B', DEVICE_OBJECTS.get('PID_SERIAL_NUMBER')) - #self.body += struct.pack('!H', 0x1001) + # self.body += struct.pack('!B', DEVICE_OBJECTS.get('PID_SERIAL_NUMBER')) + # self.body += struct.pack('!H', 0x1001) self.body += struct.pack('!B', 0x10) self.body += struct.pack('!B', 0x00) @@ -1129,14 +1186,13 @@ def _unpack_knx_body(self, message): self.body['sequence_counter'] = self._unpack_stream('!B', message) self.body['reserved'] = self._unpack_stream('!B', message) # cEMI - #self.body['cemi'] = self._unpack_cemi(message) + # self.body['cemi'] = self._unpack_cemi(message) self.body['the_end'] = message.read() except Exception as e: LOGGER.exception(e) class KnxDeviceConfigurationAck(KnxMessage): - def __init__(self, message=None, communication_channel=None, sequence_count=0): super(KnxDeviceConfigurationAck, self).__init__() if message: @@ -1148,10 +1204,10 @@ def __init__(self, message=None, communication_channel=None, sequence_count=0): self.pack_knx_message() def _pack_knx_body(self): - self.body = struct.pack('!B', 4) # structure_length - self.body += struct.pack('!B', self.communication_channel) # channel id - self.body += struct.pack('!B', self.sequence_count) # sequence counter - self.body += struct.pack('!B', 0) # status + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', self.communication_channel) # channel id + self.body += struct.pack('!B', self.sequence_count) # sequence counter + self.body += struct.pack('!B', 0) # status return self.body def _unpack_knx_body(self, message): @@ -1165,6 +1221,81 @@ def _unpack_knx_body(self, message): LOGGER.exception(e) -# TODO: implement routing requests (multicast?) -# ROUTING_INDICATION -# ROUTING_LOST_MESSAGE \ No newline at end of file +class KnxRoutingIndication(KnxMessage): + def __init__(self, message=None, knx_source='0.0.0', knx_destination=None): + super(KnxRoutingIndication, self).__init__() + if message: + self.unpack_knx_message(message) + else: + self.header['service_type'] = KNX_MESSAGE_TYPES.get('ROUTING_INDICATION') + if knx_source: + self.set_knx_source(knx_source) + if knx_destination: + self.set_knx_destination(knx_destination) + + def _pack_knx_body(self, cemi=None): + self.body = b'' + if cemi: + self.body += cemi + else: + self.body += self._pack_cemi() + return self.body + + def _unpack_knx_body(self, message): + try: + message = io.BytesIO(message) + self.body['cemi'] = self._unpack_cemi(message) + except Exception as e: + LOGGER.exception(e) + + +class KnxRoutingLostMessage(KnxMessage): + def __init__(self, message=None): + super(KnxRoutingLostMessage, self).__init__() + if message: + self.unpack_knx_message(message) + else: + self.header['service_tye'] = KNX_MESSAGE_TYPES.get('ROUTING_LOST_MESSAGE') + self.pack_knx_message() + + def _pack_knx_body(self): + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', 0) # device state + self.body += struct.pack('!H', 0) # number of lost messages + return self.body + + def _unpack_knx_body(self, message): + try: + message = io.BytesIO(message) + self.body['structure_length'] = self._unpack_stream('!B', message) + self.body['device_state'] = self._unpack_stream('!B', message) + self.body['lost_messages'] = self._unpack_stream('!H', message) + except Exception as e: + LOGGER.exception(e) + + +class KnxRoutingBusy(KnxMessage): + def __init__(self, message=None): + super(KnxRoutingBusy, self).__init__() + if message: + self.unpack_knx_message(message) + else: + self.header['service_type'] = KNX_MESSAGE_TYPES.get('ROUTING_BUSY') + self.pack_knx_message() + + def _pack_knx_body(self): + self.body = struct.pack('!B', 4) # structure_length + self.body += struct.pack('!B', 0) # device state + self.body += struct.pack('!H', 0) # routing busy wait time + self.body += struct.pack('!H', 0) # routing busy control field + return self.body + + def _unpack_knx_body(self, message): + try: + message = io.BytesIO(message) + self.body['structure_length'] = self._unpack_stream('!B', message) + self.body['device_state'] = self._unpack_stream('!B', message) + self.body['busy_wait_time'] = self._unpack_stream('!H', message) + self.body['busy_control_field'] = self._unpack_stream('!H', message) + except Exception as e: + LOGGER.exception(e) diff --git a/libknxmap/targets.py b/libknxmap/targets.py new file mode 100644 index 0000000..243c126 --- /dev/null +++ b/libknxmap/targets.py @@ -0,0 +1,268 @@ +"""This module contains various helper classes that make handling targets and sets +of targets and results easiert.""" +import binascii +import collections +import ipaddress +import logging + +from libknxmap.data.constants import * +from libknxmap.messages import * + +__all__ = ['Targets', + 'KnxTargets', + 'BusResultSet', + 'KnxTargetReport', + 'KnxBusTargetReport', + 'print_knx_target'] + +LOGGER = logging.getLogger(__name__) + +class Targets: + """A helper class that expands provided target definitions to a list of tuples.""" + def __init__(self, targets=None, ports=3671): + self.targets = set() + self.ports = set() + if isinstance(ports, list): + for p in ports: + self.ports.add(p) + elif isinstance(ports, int): + self.ports.add(ports) + else: + self.ports.add(3671) + + if isinstance(targets, (set, list)): + self._parse(targets) + elif isinstance(targets, str): + self._parse([targets]) + + def _parse(self, targets): + """Parse all targets with ipaddress module (with CIDR notation support).""" + for target in targets: + try: + _targets = ipaddress.ip_network(target, strict=False) + except ValueError: + LOGGER.error('Invalid target definition, ignoring it: {}'.format(target)) + continue + + if '/' in target: + _targets = _targets.hosts() + + for _target in _targets: + for port in self.ports: + self.targets.add((str(_target), port)) + + +class KnxTargets: + """A helper class that expands knx bus targets to lists.""" + def __init__(self, targets): + self.targets = set() + if not targets: + self.targets = None + elif not '-' in targets and self.is_valid_physical_address(targets): + self.targets.add(targets) + else: + assert isinstance(targets, str) + if '-' in targets and targets.count('-') < 2: + # TODO: also parse dashes in octets + try: + f, t = targets.split('-') + except ValueError: + return + if not self.is_valid_physical_address(f) or \ + not self.is_valid_physical_address(t): + LOGGER.error('Invalid physical address') + # TODO: make it group address aware + elif self.physical_address_to_int(t) <= \ + self.physical_address_to_int(f): + LOGGER.error('From should be smaller then To') + else: + self.targets = self.expand_targets(f, t) + + @staticmethod + def target_gen(f, t): + f = KnxMessage.pack_knx_address(f) + t = KnxMessage.pack_knx_address(t) + for i in range(f, t + 1): + yield KnxMessage.parse_knx_address(i) + + @staticmethod + def expand_targets(f, t): + ret = set() + f = KnxMessage.pack_knx_address(f) + t = KnxMessage.pack_knx_address(t) + for i in range(f, t + 1): + ret.add(KnxMessage.parse_knx_address(i)) + return ret + + @staticmethod + def physical_address_to_int(address): + parts = address.split('.') + return (int(parts[0]) << 12) + (int(parts[1]) << 8) + (int(parts[2])) + + @staticmethod + def int_to_physical_address(address): + return '{}.{}.{}'.format((address >> 12) & 0xf, (address >> 8) & 0xf, address & 0xff) + + @staticmethod + def is_valid_physical_address(address): + assert isinstance(address, str) + try: + parts = [int(i) for i in address.split('.')] + except ValueError: + return False + if len(parts) is not 3: + return False + if (parts[0] < 1 or parts[0] > 15) or (parts[1] < 0 or parts[1] > 15): + return False + if parts[2] < 0 or parts[2] > 255: + return False + return True + + @staticmethod + def is_valid_group_address(address): + assert isinstance(address, str) + try: + parts = [int(i) for i in address.split('/')] + except ValueError: + return False + if len(parts) < 2 or len(parts) > 3: + return False + if (parts[0] < 0 or parts[0] > 15) or (parts[1] < 0 or parts[1] > 15): + return False + if len(parts) is 3: + if parts[2] < 0 or parts[2] > 255: + return False + return True + + +class BusResultSet: + # TODO: implement + + def __init__(self): + self.targets = collections.OrderedDict() + + def add(self, target): + """Add a target to the result set, at the right position.""" + pass + + +class KnxTargetReport: + + def __init__(self, host, port, mac_address, knx_address, device_serial, + friendly_name, device_status, knx_medium, project_install_identifier, + supported_services, bus_devices): + self.host = host + self.port = port + self.mac_address = mac_address + self.knx_address = knx_address + self.device_serial = device_serial + self.friendly_name = friendly_name + self.device_status = device_status + self.knx_medium = knx_medium + self.project_install_identifier = project_install_identifier + self.supported_services = supported_services + self.bus_devices = bus_devices + + def __str__(self): + return self.host + + def __repr__(self): + return self.host + + +class KnxBusTargetReport: + + def __init__(self, address, medium=None, type=None, version=None, + device_serial=None, manufacturer=None, properties=None): + self.address = address + self.medium = medium + self.type = type + self.version = version + self.device_serial = device_serial + self.manufacturer = manufacturer + self.properties = properties + + def __str__(self): + return self.address + + def __repr__(self): + return self.address + + +def print_knx_target(knx_target): + """Print a target of type KnxTargetReport in a well formatted way.""" + # TODO: make this better, and prettier. + out = dict() + out[knx_target.host] = collections.OrderedDict() + o = out[knx_target.host] + o['Port'] = knx_target.port + o['MAC Address'] = knx_target.mac_address + o['KNX Bus Address'] = knx_target.knx_address + o['KNX Device Serial'] = knx_target.device_serial + o['KNX Medium'] = KNX_MEDIUMS.get(knx_target.knx_medium) + o['Device Friendly Name'] = binascii.b2a_qp(knx_target.friendly_name.strip().replace(b'\x00', b'')).decode() + o['Device Status'] = knx_target.device_status + o['Project Install Identifier'] = knx_target.project_install_identifier + o['Supported Services'] = knx_target.supported_services + if knx_target.bus_devices: + o['Bus Devices'] = list() + + # Sort the device list based on KNX addresses + x = dict() + for i in knx_target.bus_devices: + x[KnxMessage.pack_knx_address(str(i))] = i + bus_devices = collections.OrderedDict(sorted(x.items())) + + for k, d in bus_devices.items(): + _d = dict() + _d[d.address] = collections.OrderedDict() + if hasattr(d, 'type') and \ + not isinstance(d.type, (type(None), type(False))): + _d[d.address]['Type'] = DEVICE_TYPES.get(d.type) + if hasattr(d, 'medium') and \ + not isinstance(d.medium, (type(None), type(False))): + _d[d.address]['Medium'] = KNX_BUS_MEDIUMS.get(d.medium) + if hasattr(d, 'device_serial') and \ + not isinstance(d.device_serial, (type(None), type(False))): + _d[d.address]['Device Serial'] = d.device_serial + if hasattr(d, 'manufacturer') and \ + not isinstance(d.manufacturer, (type(None), type(False))): + _d[d.address]['Manufacturer'] = d.manufacturer + if hasattr(d, 'version') and \ + not isinstance(d.version, (type(None), type(False))): + _d[d.address]['Version'] = d.version + if hasattr(d, 'properties') and \ + isinstance(d.properties, dict) and d.properties: + _d[d.address]['Properties'] = d.properties + o['Bus Devices'].append(_d) + + print() + + def print_fmt(d, indent=0): + for key, value in d.items(): + if indent is 0: + print(' ' * indent + str(key)) + elif isinstance(value, (dict, collections.OrderedDict)): + if not len(value.keys()): + print(' ' * indent + str(key)) + else: + print(' ' * indent + str(key) + ': ') + else: + print(' ' * indent + str(key) + ': ', end="", flush=True) + + if key == 'Bus Devices': + print() + for i in value: + print_fmt(i, indent + 1) + elif isinstance(value, list): + for i, v in enumerate(value): + if i is 0: + print() + print(' ' * (indent + 1) + str(v)) + elif isinstance(value, (dict, collections.OrderedDict)): + print_fmt(value, indent + 1) + else: + print(value) + + print_fmt(out) + print() diff --git a/setup.py b/setup.py index 5c28b57..633d25f 100644 --- a/setup.py +++ b/setup.py @@ -7,20 +7,20 @@ print('Python 2 is not supported') sys.exit(1) elif sys.version_info.major > 2 and \ - sys.version_info.minor < 3: + sys.version_info.minor < 3: print('Python 3.3 or newer is required') sys.exit(1) elif sys.version_info.major > 2 and \ - sys.version_info.minor == 3: + sys.version_info.minor == 3: install_require.append('asyncio') -setup( - name='KNXmap', - version='', - packages=['libknx'], - install_requires=install_require, - url='https://github.com/takeshixx/knxmap', - license='', - author='takeshix', - author_email='takeshix@adversec.com', - description='Network and bus scanner for KNX devices') +setup(name='KNXmap', + version='', + packages=['libknxmap'], + scripts=['knxmap.py'], + install_requires=install_require, + url='https://github.com/takeshixx/knxmap', + license='GNU GPLv3', + author='takeshix', + author_email='takeshix@adversec.com', + description='KNXnet/IP network and bus mapper')