diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..07f9fb1d25381955bfa006566ad44690054d19b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.py.* gitlab-language=python +knx_client_script.py.complete filter=git-crypt diff=git-crypt diff --git a/.gitmodules b/.gitmodules index 185a74b6981bfb36eb9d63029afccf1aa4a1f178..bb72878e595da0c449febd0afdc9e387494a4cf3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,8 @@ [submodule "knxnet"] path = knxnet url = ssh://git@ssh.hesge.ch:10572/adrienma.lescourt/knxnet_iot.git + update = merge [submodule "actuasim"] path = actuasim url = ssh://git@ssh.hesge.ch:10572/adrienma.lescourt/actuasim_iot.git + update = merge diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..b98bd318267a62f32a425b7bd74d220b69047f1c --- /dev/null +++ b/Makefile @@ -0,0 +1,77 @@ +.ONESHELL: + +sources := knx_client_script.py +py_artifacts := __pycache__/ +git_artifacts := .git* +lab_artifacts := labo-setup/ +py_files := $(wildcard *.py) + +.PHONY: help + +all: help + +pycheck: $(py_files) + python -m py_compile $? + +# ensure symlinks are local to the source directory +_slink: + dname=$(dir $(slink)) + lname=$(notdir $(slink)) + pushd $$dname >/dev/null + ln -sf $$lname.$(suffix) $$lname + popd >/dev/null + +incomplete: $(sources) + $(MAKE) suffix=incomplete _slink slink=$< +labo: incomplete + +unlock: + git-crypt unlock + +complete: $(sources) + $(MAKE) suffix=complete _slink slink=$< + +solution: complete + +RUSER := marco +RHOST := pcnabil +RPATH := /home/marco/IoT/KNX +deploy: complete pycheck + # use a subshell to correctly see the last newline. Rsync exclude is + # handeled via STDIN + (cat | rsync -CaLhv --exclude-from=- ./ $(RUSER)@$(RHOST):$(RPATH)) <<EOT + $(addsuffix .complete,$(sources)) + $(addsuffix .incomplete,$(sources)) + $(py_artifacts) + $(git_artifacts) + $(lab_artifacts) + EOT + + +clean: + rm -rf __pycache__/ + +reset: clean incomplete + # No-op + +define _help_msg := +Usage: + + make [target] + +Targets: + + clean remove build artifacts and specious files & dirs + complete prepare the source code for the 'solution' build + deploy deploy the (complete) app to "$(RHOST):$(RPATH)" + help guess what ;-) + incomplete prepare the source code for the 'labo' build + labo alias => 'incomplete' + pycheck syntay check of Python modules + solution alias => 'complete' + unlock decrypt sensitive files +endef + +# the shell no-op silences 'nothing to be done...' messages +help: + @: $(info $(_help_msg)) diff --git a/README.md b/README.md index 0b271c80ed26202013bb294d5afa4d922d9817d1..6e0ddff8f8ac99da691522a7d4d25150c73c453f 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,32 @@ The first part of the course is based on KNX, [an open standard for commercial a ## Scenario ## -Several KNX-compatible *actuators* for radiators valves and window shutters +Several KNX-compatible *actuators* for radiator valves and window blinds are deployed across the rooms and the floors of a (virtual) building. We want -to read the status of and send actuation commands to any of them, so as -to change a radiator's valve or a window's shutter position. +to read the status of, and send actuation commands to any of them, so as to +change a radiator's valve or a window's blinds position. Each actuator is +assigned a *group address*, which is a triple (3-level-style) like: -Each actuator is univocally identified by a group address... **[TO-DO: see doc -in `ActuatorsRESTserver/knx_client_script.py`]** + <action>/<floor>/<bloc> -Your **goal** is to write a Python-3-based client script which... **[TO-DO: -see the course's PDF doc** +where the physical location is given by `<floor>/<bloc>`, whereas `<action>` +specifies the command type. Notice that this is arbitrary, indeed the action +can change but the target actuator is the same. Thus, something like -A simulator is available to test your client script. + 1/2/3 + +may mean: do whatever is associated with action `1` (e.g., commands for a +radiator valve) to the actuator(s) located at floor 2, bloc 3. The real action +is specified further by the telegram's *payload*. + +Your **goal** is to complete a Python-3-based client script +(`knx_client_script.py`) whose CLI is already defined -- see below. A +simulator is available to test your client script -- see this repo's subdir +`actuasim/`. Provided that the needed dependencies are installed, you can +launch it with: + + $ python actuasim.py -**[TO-DO] more doc** ## Installation ## @@ -29,29 +41,112 @@ You need to install: * the support library: `knxnet` * the simulator: `actuasim` -See instructions in the corresponding sub-directories of this repository. +This Git repo holds the relative project submodules. Clone it recursively: + + $ git clone --recursive https://gitedu.hesge.ch/lsds/teaching/master/iot/knx.git + +Follow instructions (README) in the corresponding sub-directories of this +repository, or at the following links: + +* `[knxnet/README](https://gitedu.hesge.ch/adrienma.lescourt/knxnet_iot/-/blob/master/README.md "knxnet readme")` +* `[actuasim/README](https://gitedu.hesge.ch/adrienma.lescourt/actuasim_iot/-/blob/master/README.md "actuasim readme")` -**[TO-DO] more doc** ## Develop ## -See the example script in `examples/knx_client_script.py`. You shall complete -all parts marked as `@@@ TO BE COMPLETED @@@. +You shall complete all parts marked as `@@@ TO BE COMPLETED @@@` in file +`knx_client_script.py`. This CLI-based script and has two modes: raw and +target. To know more about its usage: + + $ knx_client_script.py --man + +Raw mode: + + $ knx_client_script.py raw --help -**[TO-DO] more doc** +Target mode: + + $ knx_client_script.py {blind,valve} --help ### Tests ### -**[TO-DO]** Run the test suite... +These are the tests that you shall perform against `actuasim`. The client +script is configured to talk by default to a locally installed simulator +instance. + +#### Radiator valves #### + +1. Open/close: + + knx_client_script.py raw '0/4/1' 50 2 2 + knx_client_script.py valve set '4/1' 50 + +2. Read status: + + knx_client_script.py raw '0/4/1' 0 1 0 + knx_client_script.py valve get '4/1' + +#### Window blinds #### + +1. Full open: + + knx_client_script.py raw '1/4/1' 0 1 2 + knx_client_script.py blind open '4/1 + +2. Full close: + + knx_client_script.py raw '1/4/1' 1 1 2 + knx_client_script.py blind close '4/1' + +3. Open/close: + + knx_client_script.py raw '3/4/1' 50 2 2 + knx_client_script.py blind set '4/1' 50 + +2. Read status: + + knx_client_script.py raw '4/4/1' 0 1 0 + knx_client_script.py blind get '4/1' ## Maintenance ## -* Move here +This section is for code *maintainers*. Students are however encouraged to +read :-) + +Most tasks are available via the included `Makefile`. See: + + $ make help + +Source code for the solution build and other sensitive files are encrypted. If +you have just cloned this repo and need to work on sensitive stuff, **the +first thing to do** is to run (your GPG public key must be registered by a +repo's owner/maintainer -- see +[here](https://githepia.hesge.ch/lsds-collab-test/cours-x/project-1/-/blob/master/README.md +"Git-crypted test repo help")): + + $ make unlock + +Unlocked content will be anyway encrypted on subsequent push operations. It +won't work if the working dir is dirty! Labo's files usually come in two +versions: `<whatever>.complete` and `<whatever>.incomplete`. At any time, only +one of them (by default the `.incomplete`)is simlinked from a deployable +`<whatever>` file -- i.e., this latter is a symlink and should never, ever by +removed. Thus , you can just edit `<whatever>` + +It is good practice to have this repo labo-ready, thus *before* pushing new +stuff, you should always call: + + $ make reset + +To deploy a (complete) build/solution somewhere, use (remot user, host and path might +need to be adapted): + + $ [RUSER=... RHOST=... RPATH=...] make deploy + - https://gitedu.hesge.ch/lsds/Smarthepia/-/blob/master/ActuatorsRESTserver/knx_client_script.py (to be encrypted) - https://gitedu.hesge.ch/lsds/Smarthepia/-/tree/master/ActuatorsRESTserver/tests +### TO-DO ### * Provide an install script * Provide a test harness diff --git a/examples/knx_client_script.py b/examples/knx_client_script.py deleted file mode 100644 index 5ed34c1711a97692416fcef84d50e422108c3b37..0000000000000000000000000000000000000000 --- a/examples/knx_client_script.py +++ /dev/null @@ -1 +0,0 @@ -# WORK IN PROGRESS: place holder for an example script" diff --git a/knx_client_script.py b/knx_client_script.py deleted file mode 100644 index cd9ca8b66a85ee79f44e737c955c4ddd21e74801..0000000000000000000000000000000000000000 --- a/knx_client_script.py +++ /dev/null @@ -1,2 +0,0 @@ -# WORK IN PROGRESS: place holder for the *complete* solution to be crypted and -# managed via git-crypt diff --git a/knx_client_script.py b/knx_client_script.py new file mode 120000 index 0000000000000000000000000000000000000000..b8dbe1997e1b5f7d2a2eb35618cb4de91b2d5f1e --- /dev/null +++ b/knx_client_script.py @@ -0,0 +1 @@ +knx_client_script.py.incomplete \ No newline at end of file diff --git a/knx_client_script.py.complete b/knx_client_script.py.complete new file mode 100755 index 0000000000000000000000000000000000000000..5f79210fbd9d966815e21bcce7651d5436018047 Binary files /dev/null and b/knx_client_script.py.complete differ diff --git a/knx_client_script.py.incomplete b/knx_client_script.py.incomplete new file mode 100755 index 0000000000000000000000000000000000000000..19eeb940334500d2f9934a337de4c4a1268c3e7b --- /dev/null +++ b/knx_client_script.py.incomplete @@ -0,0 +1,596 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; mode: Python -*- +################################################################################ +"""knx_client_script.py -- demo client for UDP-based KNX deployments (ETS) @ +Smarthepia + +Usage: + + 'raw' mode: + + python3 knx_client_script.py [OPTIONS] raw GROUP_ADDRESS PAYLOAD + + 'target' mode: + + python3 knx_client_script.py [OPTIONS] TARGET COMMAND ADDRESS [VALUE] + + +Arguments +========= + +'raw' mode +++++++++++ + + GROUP_ADDRESS + <str>. Triple of integers formatted as '<action>/<floor>/<bloc>' in + [0, 31]/[0, 7]/[0, 255] + + PAYLOAD is a 3-int tuple 'DATA DATA_SIZE APCI': + + DATA + <int>. Command data in [0, 255] + + SIZE + <int>. Data size in bytes. Valid range is [1, 2] + + APCI + <int>. Application Layer Protocol Control Information (service ID) + command: 0 for group value read (get), or 2 for group value write + (set) + + +See L<Deployment configuration> for details. + +'target' mode ++++++++++++++ + + TARGET + <str>. Actuator/sensor class in ['blind', 'valve'] + + COMMAND + <str>. Command, depending on chosen target + + ADDRESS + <str>. Pair of integers formatted as '<floor>/<bloc>' in [0, 7]/[0, 255] + + VALUE + <int>. Only for command 'set', in percent. + + +Global options +============== + + -i GATEWAY_IP_ADDRESS + <str>. Use '129.194.184.31' for the physical gateway @ Smarthepia, or + '127.0.0.1' for a local simulator + + -p GATEWAY_UDP_PORT + <int>. Obvious + + -c CONTROL_ENDPOINT + <str>. Control endpoint -- use '0.0.0.0:0' with the physycal gateway + + -d DATA_ENDPOINT + <str>. Data endpoint -- use '0.0.0.0:0' with the physycal gateway + + -l LOG_LEVEL + <str>. Logging level + +Call with '-h' to see help for all options. + + +Deployment configuration +======================== + +Legend: + +* GADDR = GROUP_ADDRESS ("a/f/b"), +* 'a' = action +* 'f' = floor +* 'b' = bloc +* "don't-care" = any integer + +N.B. Action codes in GADDR for blinds and valves are not homogeneous! + + +Blind's control ++++++++++++++++ + + +action DATA SIZE APCI GADDR status +---------------------------------------------------------------- +full open (set) 0 1 2 1/f/b OK +---------------------------------------------------------------- +full close (set) 1 1 2 1/f/b OK +---------------------------------------------------------------- +read state (get) (don't-care) 1 0 4/f/b OK +---------------------------------------------------------------- +partial open/close [0...255] 2 2 3/f/b OK +(set) 0: 100% opened + 255: 100% closed +---------------------------------------------------------------- + +N.B. Different action codes are used for the various operations. + + +Valve's control ++++++++++++++++ + + +action DATA SIZE APCI GADDR status +---------------------------------------------------------------- +read state (don't-care) 1 0 0/f/b OK +---------------------------------------------------------------- +partial open/close [0...255] 2 2 0/f/b OK + 0: 100% closed + 255: 100% opend +---------------------------------------------------------------- + +N.B. The same action code is used for all the operations. + + +Examples +======== + +Raw mode +++++++++ + +Fully open (set) a blind @ floor 4, bloc 1 (notice a = 1): + + $ python3 knx_client_script.py raw '1/4/1' 0 1 2 + + +Partially close/open [0,255] (set) a blind @ floor 4, bloc 1 (notice a = +3): + + $ python3 knx_client_script.py raw '3/4/1' 100 2 2 + + +Then, read (get) the blind status (notice a = 4, 1234 is used as "don't care" +value): + + $ python3 knx_client_script.py raw '4/4/1' 1234 1 0 + ... + Result: 100 + + +Target mode ++++++++++++ + +Fully open a blind @ floor 4, bloc 1: + + $ python3 knx_client_script.py blind open '4/1' + + +Partially open/close (set 30%) a blind @ floor 4, bloc 1: + + $ python3 knx_client_script.py blind set '4/1' 30 + + +Then, read (get) the blind status: + + $ python3 knx_client_script.py blind get '4/1' + ... + Result: 77 + + +Bugs +==== + +Mostly caused by the fact that SmartHepia deployment is not homogenous. + +1. With `actuasim`: a wrong action code might hang the protocol (no final +tunnelling request). + +2. Invalid SIZE can crash `actuasim`. Should catch "bad frame" errors. + + +To-Do +===== + +* Clarify control and data endpoints + + +See Also +======== + +* Smarthepia Web site L<http://lsds.hesge.ch/smarthepia/> +""" +################################################################################ +import argparse, socket, sys, logging +from knxnet import * + +logger = None +buf_size = 1024 # bytes + +cmmnd_ref = { + 'blind' : { + 'get' : { + 'data' : 1234, # "don't-care" + 'size' : 1, + 'apci' : 0, + 'action' : 4, + }, + 'set' : { + 'data' : None, # from input as 'value' + 'size' : 2, + 'apci' : 2, + 'action' : 3, + }, + 'open' : { + 'data' : 0, + 'size' : 1, + 'apci' : 2, + 'action' : 1, + }, + 'close' : { + 'data' : 1, + 'size' : 1, + 'apci' : 2, + 'action' : 1, + } + }, + 'valve' : { + 'get' : { + 'data' : 1234, # "don't-care" + 'size' : 1, + 'apci' : 0, + 'action' : 0, + }, + 'set' : { + 'data' : None, # from input as 'value' + 'size' : 2, + 'apci' : 2, + 'action' : 0, + }, + } +} + +################################################################################ +def validate_percent_int(string): + """Scream if input `string` is not an int in [0,100] + + :param str string: the input value to validate + + :returns int: the converted valid input value + + :raises argparse.ArgumentTypeError: when the input is invalid + """ + value=int(string) + if value < 0 or value > 100: + raise argparse.ArgumentTypeError("{}: must be integer in [0, 100]".format(string)) + + return value + + +def build_target_command(target, args): + """Build a specific "target" command, using the global `cmmnd_ref` dict as a + reference. + + :param target str: the target command + + :param args namespace: the full argparse namespace + + :returns list: + + str gaddress: the KNX group address + + str payload: the KNX payload + + :raises ValueError: when the a value is not provided for a target called + with a 'set' command + """ + logger.debug('args: {}'.format(args)) + + if target == 'raw': + return knxnet.GroupAddress.from_str(args.group_address), args.payload + + # high-level command mode + cmnd_def = cmmnd_ref[target][args.command] + logger.debug('cmnd_def: {}'.format(cmnd_def)) + gaddress = knxnet.GroupAddress.from_str(str(cmnd_def['action']) + '/' + args.address) + + payload = [cmnd_def[k] for k in ('data', 'size', 'apci')] + if args.command == 'set': + if args.value == None: + raise ValueError("'set' command requires a 'value'") + # rescale + value = int(args.value/100*255) + # 'set' data + payload[0] = value + + return gaddress, payload + + +def send_knx_request( + dest_group_addr, + payload, # data, data_size, apci + gateway_ip='127.0.0.1', + gateway_port='3671', + control_endpoint='127.0.0.1:3672', + data_endpoint='127.0.0.1:3672', +): + """Send a request to a KNX gateway for a destination group address with a + payload. + + Arguments + +++++++++ + + :param dest_group_addr str: group address like 'ACTION/FLOOR/BLOCK' + + :param payload list: a tuple [DATA, DATA_SIZE, APCI] + + + Keyword args + ++++++++++++ + + :param gateway_ip str: the gateway's IP address + + :param gateway_port str: the gateway's IP PORT + + :param control_endpoint str: the control endpoint as 'IP-ADDRESS:PORT' + + :param data_endpoint str: the data endpoint as 'IP-ADDRESS:PORT' + + :returns list: (status, reply) + + bool status: success/failure code + + str reply: a numerical value for get commands or a textual status code + for anything else + """ + data, data_size, apci = payload + + gateway_port = int(gateway_port) + + control_endpoint = control_endpoint.split(':') + control_endpoint = tuple([control_endpoint[0], int(control_endpoint[1])]) + data_endpoint = data_endpoint.split(':') + data_endpoint = tuple([data_endpoint[0], int(data_endpoint[1])]) + + local_ip_addr = control_endpoint[0] + local_udp_port = control_endpoint[1] + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', local_udp_port)) + + # -> Connection request + conn_req = knxnet.create_frame( + knxnet.ServiceTypeDescriptor.CONNECTION_REQUEST, + control_endpoint, + data_endpoint + ) + logger.debug('>>> CONNECTION_REQUEST:\n{}'.format(conn_req)) + sock.sendto(conn_req.frame, (gateway_ip, gateway_port)) + + # <- Connection response + logger.debug('waiting for a connection response (CONNECTION_RESPONSE -> conn_resp)') + data_recv, addr = sock.recvfrom(buf_size) + conn_resp = knxnet.decode_frame(data_recv) + logger.info('reponse status: {}'.format(conn_resp.status)) + logger.info('reponse data endpoint: {}'.format(conn_resp.data_endpoint)) + + ############################################################################ + # @@@ TO BE COMPLETED @@@ + # >>> START OF YOUR WORK + ############################################################################ + + # So far, only the first 2 steps of a full + # protocol round were done (CONNECTION_REQUEST/RESPONSE). Keep in mind + # that: + + # you have to ckeck status codes to see if the the command was correctly + # understood, and _disconnect_ otherwise (DISCONNECT_REQUEST). Then again + # check if that went through; + + # the `acpi` tells you if you're handling a get (read) or a set (write) + # command. + + # <<< END OF YOUR WORK + ############################################################################ + + return False, 'Error' + + +################################################################################ +if __name__ == "__main__": + logging.basicConfig( + # level=logging.WARNING, + format='%(levelname)-8s %(message)s' + ) + logger = logging.getLogger('knx_client') + + ############################################################################ + # main parser + ############################################################################ + parser = argparse.ArgumentParser( + description='Demo client for UDP-based KNX deployments (ETS) @ Smarthepia', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + '-m', '--manual', + dest='manual', + action='store_true', + help='print the full documentation' + ) + + parser.add_argument( + '-i', '--gateway-ip-address', + dest='gateway_ip_address', + type=str, + default='127.0.0.1', + help="Gateway's IP address" + ) + + parser.add_argument( + '-p', '--gateway-udp-port', + dest='gateway_udp_port', + type=str, + default='3671', + help="Gateway's UDP port" + ) + + parser.add_argument( + '-c', '--control-endpoint', + dest='control_endpoint', + type=str, + default='127.0.0.1:3672', + help="Control endpoint -- use '0.0.0.0:0' for NAT" + ) + + parser.add_argument( + '-d', '--data-endpoint', + dest='data_endpoint', + type=str, + default='127.0.0.1:3672', + help="Data endpoint -- use '0.0.0.0:0' for NAT" + ) + + parser.add_argument( + '-l', '--log-level', + dest='log_level', + type=str, + default='ERROR', + choices=( + 'CRITICAL', + 'ERROR', + 'WARNING', + 'INFO', + 'DEBUG', + ), + help="Logging level" + ) + + ############################################################################ + # subcommands + ############################################################################ + subparsers = parser.add_subparsers( + title='targets', + dest='subparser_name', + help='Target actuator/sensor ("raw" is for low-level KNX-style)' + ) + + # @raw target + parser_raw = subparsers.add_parser( + 'raw', + help='send low-level KNX-style commands, such as "-g ACTION/FLOOR/BLOCK DATA SIZE ACPI"', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser_raw.add_argument( + 'group_address', + type=str, + help='KNX group address "<action>/<floor>/<bloc>"' + ) + + parser_raw.add_argument( + 'payload', + # no metavar, else choices won't be proposed on -h + type=int, + nargs=3, + help="3-int tuple 'DATA DATA_SIZE APCI'" + ) + + # @blind target + parser_blind = subparsers.add_parser( + 'blind', + help='send high-level commands to blinds, such as "get FLOOR/BLOCK"', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser_blind.add_argument( + 'command', + type=str, + choices=('open', 'close', 'get', 'set'), + help='Blind command (get == read, set == write)' + ) + + parser_blind.add_argument( + 'address', + type=str, + help='Blind address "<floor>/<bloc>"' + ) + + parser_blind.add_argument( + 'value', + nargs='?', + type=validate_percent_int, + help='Set value percent [0, 100]' + ) + + # @valve target + parser_valve = subparsers.add_parser( + 'valve', + help='send high-level commands to valves, such as "get FLOOR/BLOCK"', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser_valve.add_argument( + 'command', + type=str, + choices=('get', 'set'), + help='Valve command (get == read, set == write)' + ) + + parser_valve.add_argument( + 'address', + type=str, + help='Valve address "<floor>/<bloc>"' + ) + + parser_valve.add_argument( + 'value', + nargs='?', + type=validate_percent_int, + help='Set value percent [0,100]' + ) + + ############################################################################ + + args = parser.parse_args() + + if args.manual: + print(__doc__) + sys.exit(0) + + logger.debug("target: {}".format(args.subparser_name)) + target = args.subparser_name + if not target: + parser.print_usage() + sys.exit('\nPlease provide a "target"') + + logger.setLevel(args.log_level) + + # parse high-level command + try: + dest_addr, payload = build_target_command(target, args) + except ValueError as e: + sys.exit("Can't build target command: {}".format(e)) + + logger.info( + 'going to send command: {} (group address) {} (payload)'.format(dest_addr, payload) + ) + + reply = send_knx_request( + dest_addr, + payload, + gateway_ip=args.gateway_ip_address, + gateway_port=args.gateway_udp_port, + control_endpoint=args.control_endpoint, + data_endpoint=args.data_endpoint, + ) + + logger.debug('reply: {}'.format(reply)) + + status = 0 if reply[0] else 1 + result = reply[1] + try: + result = int(result) + result = '{} ({}%)'.format(result, int(result/255*100)) + except ValueError: + pass + + print('Result: {}'.format(result)) + + sys.exit(status) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ae860f4219f9ce2dad9a13bd2e8211edf409d856 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,58 @@ +Tests for smarthepia deployment +=============================== + +KNX gateway address: 129.194.184.31 +Video monitor: http://129.194.184.223/video1.mjpg +Terminal login: ssh 129.194.186.252 + +First, set a command alias ;-) + + $ alias knx='knx_client_script.py -i 129.194.184.31 -c 0.0.0.0:0 -d 0.0.0.0:0' + + +#### Radiator valves #### + +1. Open/close: + + knx raw '0/4/10' 50 2 2 + knx valve set '4/10' 50 + +2. Read status: + + knx raw '0/4/10' 0 1 0 + knx valve get '4/10' + +#### Window blinds #### + +1. Full open: + + knx raw '1/4/10' 0 1 2 + knx blind open '4/1 + +2. Full close: + + knx raw '1/4/10' 1 1 2 + knx blind close '4/10' + +3. Open/close: + + knx raw '3/4/10' 50 2 2 + knx blind set '4/10' 50 + +2. Read status: + + knx raw '4/4/10' 0 1 0 + knx blind get '4/10' + + +Bugs +==== + +Read status returns the actual value + 1. + + +To do +===== + +* Scriptify the above tests ;-) +* Provide a test harness