Skip to content
Snippets Groups Projects
Commit 6eec668b authored by marcoemi.poleggi's avatar marcoemi.poleggi
Browse files

Doc + new client + util

parent 73db85d6
Branches
No related tags found
No related merge requests found
# Smart Building Lab 1 / Part 2: Z-Wave #
This is a template of a Python flask-based application for controlling a
Z-Wave IoT deployment.
The `backend.py` file is to be completed in the parts marked by:
```
#### COMPLETE THIS METHOD ####
```
Once done, you can start your server with the command
```
python2 flask-main.py
```
then, wait ~1 minute for the network to stabilize, and check the connection with a browser,
normally at the URL:
```
http://192.168.1.2:5000
```
## Raspberry set-up ##
In order to share a network card as an internet gateway in Linux, ip
[forwarding and masquerading must be setup](https://wiki.archlinux.org/index.php/Internet_sharing). The
`util/internet-sharing` script does the trick:
```
#!/bin/bash
################################################################################
# internet-sharing
#
# Forward outbound traffic from a client interface to an internet gateway
# interface.
#
# <https://wiki.archlinux.org/index.php/Internet_sharing>
# <https://linoxide.com/firewall/ip-forwarding-connecting-private-interface-internet/>
################################################################################
gwint=${1:-'net0'} # internet gateway interface -- all outbound traffic
clint=${2:-'net1'} # client interface -- input traffic
sysctl net.ipv4.ip_forward=1 net.ipv6.conf.default.forwarding=1 net.ipv6.conf.all.forwarding=1
iptables -t nat -A POSTROUTING -o $gwint -j MASQUERADE
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i $clint -o $gwint -j ACCEPT
```
By default, it expects two interfaces named `net0` and `net1` for,
respectively, the internet gateway and the client inbound interface. So, if
you want to reroute all incoming traffic from, say, `eth0` (where your
Raspberry is connected to) through your wireless connection `wlan0`, you'd call
the script like this (as **superuser**):
```
# ./internet-sharing wlan0 eth0
```
To make the changes persistent, consult your Linux distribution's documentation.
## Testing ##
A CL-based client script is available: `post_client.py`. See how to use it with
```
python3 post_client.py -h
```
import requests
import json
#!/usr/bin/env python3
################################################################################
"""post_client.py -- demo client script for hepia's LSDS IoT Z-Wave Lab
#############################################################
#### This script sends POST or PUT http request to server ###
#############################################################
#### You have to uncomment the request you want to send ###
#############################################################
#### POST requests contain parameters in JSON format ###
#############################################################
This script sends POST or GET HTTP requests to a REST server. POST requests
contain parameters in JSON format.
Usage:
#### Configuration of nodes
req = requests.post('http://192.168.1.2:5000/network/set_sensor_nodes_basic_configuration',headers={'Content-Type': 'application/json'}, data=json.dumps({'Group_Interval': '240','Group_Reports':'240', 'Wake-up_Interval': '480'}))
post_client.py [OPTIONS] CLASS COMMAND
See `post_client.py -h` for the details
#### Config a specific parameter
#req = requests.post('http://192.168.1.2:5000/nodes/set_parameter',headers={'Content-Type': 'application/json'}, data=json.dumps({'node_id': '4','value':'480', 'parameter_index': '111', 'size': '4'}))
REST command paths
==================
#### Set node location
#req = requests.post('http://192.168.1.2:5000/nodes/set_location',headers={'Content-Type': 'application/json'}, data=json.dumps({'node_id': '4','value':'A402'}))
GET requests
------------
URL path:
#### Set node name
#req = requests.post('http://192.168.1.2:5000/nodes/set_name',headers={'Content-Type': 'application/json'}, data=json.dumps({'node_id': '4','value':'sensor'}))
/<class>[/<node_id>]/<command>[/<parameter_index>]
where `node_id` and `parameter_index` must be specified via explicit CL
options, respectively `-n` and `-i`.
#### Send command to switch
#req = requests.post('http://192.168.1.2:5000/switches/send_command',headers={'Content-Type': 'application/json'}, data=json.dumps({'node_id': '3','value':'on'}))
#### Send command to dimmer
#req = requests.post('http://192.168.1.2:5000/dimmers/set_level',headers={'Content-Type': 'application/json'}, data=json.dumps({'node_id': '6','value':'120'}))
POST requests
-------------
#### Put controller in inclusion mode
#req = requests.post('http://192.168.1.2:5000/nodes/add_node')
URL path:
/<class>/<command>
#### Put controller in exclusion mode
#req = requests.post('http://192.168.1.2:5000/nodes/remove_node')
with JSON payload specified via the CL option `-d`.
print (req.text) # print server response
Examples
========
Dump network topology:
post_client.py network info
Switch to inclusion mode (20s timeout):
post_client.py nodes add_node
Switch to exclusion mode (20s timeout):
post_client.py nodes remove_node
Get dimmer level for a specific node (given as CL option `-n`):
post_client.py dimmers get_level -n 5
Set dimmer level for a specific node (notice how the ID is specified in
the JSON payload):
post_client.py dimmers set_level -d '{"node_id": 5, "value": 50}'
Get value for a generic parameter at index '111' for node '5':
python3 post_client.py nodes get_parameter -n 5 -i 111
Set value for a generic parameter at index '111' for node '5':
python3 post_client.py nodes set_parameter \
-d '{"node_id": 5, "value": 480, "parameter_index": 111, "size": 4}'
Get all measures for a given node
post_client.py sensors get_all_measures -n 2
Bugs
====
* At least CLASS and COMMAND must be given to get the script's manual with
`-m`.
* Setting generic parameter doesn't seem to work properly: (the parameter is
created if not existing,) it's value stays always at 0.
"""
################################################################################
import requests, argparse, json, sys, copy, logging
logging.basicConfig(
level=logging.INFO,
format='%(levelname)-8s %(message)s'
)
logger = logging.getLogger('rest_client')
################################################################################
# constants
http_headers = {
'Content-Type' : 'application/json'
}
# cross reference with default node ID, par index and data (where needed)
command_xref = {
'network' : {
'get_nodes_configuration' : {
'method' : 'GET',
},
'info' : {
'method' : 'GET',
},
'reset' : { # possibly broken
'method' : 'GET',
},
'set_sensor_nodes_basic_configuration' : {
'method' : 'POST',
'data' : {
'Group_Interval': '240',
'Group_Reports': '240',
'Wake-up_Interval': '480'
}
},
'start' : {
'method' : 'GET',
},
'stop' : {
'method' : 'GET',
},
},
'nodes' : {
'add_node' : {
'method' : 'POST',
},
'get_battery' : {
'method' : 'GET',
'node_id' : 1,
},
'get_location' : {
'method' : 'GET',
'node_id' : 1,
},
'get_name' : {
'method' : 'GET',
'node_id' : 1,
},
'get_neighbours' : {
'method' : 'GET',
'node_id' : 1,
},
'get_nodes_list' : {
'method' : 'GET',
},
'get_parameter' : {
'method' : 'GET',
'node_id' : 1,
'parameter_index' : 111,
},
'remove_node' : {
'method' : 'POST',
},
'set_location' : {
'method' : 'POST',
'data' : {
'node_id' : '1',
'value' : 'A402'
}
},
'set_name' : {
'method' : 'POST',
'data' : {
'node_id' : '2',
'value' : 'sensor'
}
},
'set_parameter' : {
'method' : 'POST',
'data' : {
'node_id' : '3',
'value' : '480',
'parameter_index' : '111',
'size' : '4'
}
}
},
'dimmers' : {
'get_dimmers_list' : {
'method' : 'GET',
},
'get_level' : {
'method' : 'GET',
'node_id' : 1,
},
'set_level' : {
'method' : 'POST',
'data' : {
'node_id': '5',
'value': '10'
}
}
},
'sensors' : {
'get_all_measures' : {
'method' : 'GET',
'node_id' : 2,
},
'get_humidity' : {
'method' : 'GET',
'node_id' : 2,
},
'get_luminance' : {
'method' : 'GET',
'node_id' : 2,
},
'get_motion' : {
'method' : 'GET',
'node_id' : 2,
},
'get_sensors_list' : {
'method' : 'GET',
},
'get_temperature' : {
'method' : 'GET',
'node_id' : 2,
},
},
}
################################################################################
# functions
def build_request_path(
cclass, command, cmnd_s,
node_id=None,
parameter_index=None,
):
"""Build a request path.
:returns str: a string like '/<class>[/<node_id>]/<command>[/<parameter>]'
or None on errors
Arguments
+++++++++
:param str cclass: the resource/object class
:param str command: the command name
:param dict cmnd_s: the command spec dict as in `command_xref`
Keywords arguments
++++++++++++++++++
:param int node_id: the target node to query
:param int parameter_index: the parameter index to query
"""
if 'node_id' in cmnd_s.keys():
node_id = node_id or cmnd_s['node_id']
elif node_id:
logger.error(
"/{}/{}: unexepected 'node_id' ({}) for this request".format(
cclass, command, node_id
)
)
return None
if 'parameter_index' in cmnd_s.keys():
parameter_index = parameter_index or cmnd_s['parameter_index']
elif parameter_index:
logger.error(
"/{}/{}: unexepected 'parameter_index' ({}) for this request".format(
cclass, command, parameter_index
)
)
return None
return '/' + cclass + '/' \
+ (( str(node_id) + '/' ) if node_id != None else '') + command \
+ (( '/' + str(parameter_index) ) if parameter_index != None else '')
################################################################################
# main
parser = argparse.ArgumentParser(
description='Demo client for REST-based Z-Wave deployments -- hepia/LSDS Smart-Building',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
'-m', '--manual',
dest='manual',
action='store_true',
help='print the full documentation'
)
parser.add_argument(
'-u', '--server-url',
dest='server_url',
type=str,
default='http://192.168.1.2',
help='REST server URL'
)
parser.add_argument(
'-p', '--server-port',
dest='server_port',
type=int,
default=5000,
help='REST server PORT'
)
parser.add_argument(
'-n', '--node-id',
dest='node_id',
type=int,
help='Node ID'
)
parser.add_argument(
'-d', '--data',
dest='data',
type=str,
help='JSON payload string for POST requests'
)
parser.add_argument(
'-i', '--parameter-index',
dest='parameter_index',
type=int,
help='Parameter index'
)
parser.add_argument(
'class',
# no metavar, else choices won't be proposed on -h
type=str,
choices=('network', 'nodes', 'dimmers', 'sensors'),
help='Device/object class'
)
parser.add_argument(
'command',
type=str,
choices=(
# network
'get_nodes_configuration',
'info',
'reset',
'set_sensor_nodes_basic_configuration',
'start',
'stop',
# nodes
'add_node',
'get_battery',
'get_location',
'get_name',
'get_neighbours',
'get_nodes_list',
'get_parameter',
'remove_node',
'set_location',
'set_name',
'set_parameter',
# dimmers
'get_dimmers_list',
'get_level',
'set_level',
# sensors
'get_all_measures',
'get_humidity',
'get_luminance',
'get_motion',
'get_sensors_list',
'get_temperature'
),
help='Command to send'
)
# easier with a dict
args = vars(parser.parse_args())
if args['manual']:
print(__doc__)
sys.exit(0)
cclass = args['class']
command = args['command']
data = {}
if args['data']:
try:
data = json.loads(args['data'])
except Exception as e:
logger.error("{}: invalid JSON data: {}".format(args['data'], e))
sys.exit(1)
cmnd_s = {}
try:
cmnd_s = command_xref[cclass][command]
except KeyError:
parser.print_help()
logger.error("{}: {}: no such command available for this class".format(cclass, command))
sys.exit(1)
base_url = "{}:{}".format(args['server_url'], args['server_port'])
command_path = build_request_path(
cclass, command, cmnd_s,
node_id=args['node_id'],
parameter_index=args['parameter_index']
)
if not command_path:
logger.fatal("Can't build command path")
sys.exit(1)
method = cmnd_s['method']
if 'data' in cmnd_s.keys():
data = data or cmnd_s['data']
elif data:
sys.exit("{}: unexpected data for command path".format(command_path))
logger.info("Sending {} request path: '{}'".format(method, command_path))
if data:
logger.info("With payload: '{}'".format(data))
req = requests.request(
method, base_url + command_path,
headers=http_headers,
data=json.dumps(data)
)
print("Server response:\n[{}] {}".format(req.status_code, req.text))
sys.exit(0)
#!/bin/bash
################################################################################
# internet-sharing
#
# Forward outbound traffic from a client interface to an internet gateway
# interface.
#
# <https://wiki.archlinux.org/index.php/Internet_sharing>
# <https://linoxide.com/firewall/ip-forwarding-connecting-private-interface-internet/>
################################################################################
gwint=${1:-'net0'} # internet gateway interface -- all outbound traffic
clint=${2:-'net1'} # client interface -- input traffic
sysctl net.ipv4.ip_forward=1 net.ipv6.conf.default.forwarding=1 net.ipv6.conf.all.forwarding=1
iptables -t nat -A POSTROUTING -o $gwint -j MASQUERADE
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i $clint -o $gwint -j ACCEPT
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment