diff --git a/README.md b/README.md
index 7a62c7b904617294c45d2a32877266d5e7936fae..8b04c2eddd9b87e8558d36ac594d2a782bfc6ec5 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,22 @@ Setup your server as follows:
2. If you are not using MQTT or HTTP leave out the respective lines.
## Recent changes
+7.0.0
+- Completely reworked the architecture of the library
+ - changed from *framework* to more convenient *library* structure
+
+- major changes
+ - no JSON-model and mapping required anymore
+ - json-model is no deduced from the class structure
+
+- removed deprecated features
+ - docstring parsing due to its error-prone behaviour
+
+- refactoring
+ - simplified StreamScheduler to a single class for all jobs (instead of dedicated classes for different job types)
+ - renamed class Figure to Variable
+ - jobs are now stored within the Variable the job belongs to
+
6.0.2 | 5.2.5
- bug fix
- fixed parsing of parameters and variables/ measurements of type "time" for higher dimensions
diff --git a/Todo.md b/Todo.md
new file mode 100644
index 0000000000000000000000000000000000000000..f5f06835601db9aa261115cd578d9c5e8e33fd11
--- /dev/null
+++ b/Todo.md
@@ -0,0 +1,10 @@
+# Todos
+
+- [ ] refactor component
+- [ ] refactor function
+- [ ] figure out how to implement arguments and returns
+- [ ] improve error handling
+- [ ] improve verbosity (i.e. the thrown errors)
+- [ ] apply new structure to example laser tracker
+- [ ] handover sibling parameter if used as dynamic interval for a job
+- [ ] develop a nice solution how to hand over mqtt publish method to a function without passing it down the complete component tree
\ No newline at end of file
diff --git a/build.bat b/build.bat
deleted file mode 100644
index 1b357428ac738cb4b5d780811941e972bab9ab03..0000000000000000000000000000000000000000
--- a/build.bat
+++ /dev/null
@@ -1,7 +0,0 @@
-@echo off
-rmdir dist /s /q
-rmdir build /s /q
-rmdir wzl_udi.egg-info /s /q
-python setup.py bdist_wheel
-:: TODO complete line below
-:: copy "../dist/" "D:\users\bdn\Forschungsprojekte\Virtual Metrology Frame\SourceCode\PyPi server"
\ No newline at end of file
diff --git a/classes.png b/classes.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ebeb11d1bb4e48441b3d012c6c9cb886c69c470
Binary files /dev/null and b/classes.png differ
diff --git a/environment.yml b/environment.yml
deleted file mode 100644
index 96849da192beff519b92f98efa20101f6454a8f6..0000000000000000000000000000000000000000
--- a/environment.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: udi
-channels:
- - conda-forge
- - defaults
-dependencies:
- - aiohttp=3.7.3=py37hcc03f2d_0
- - async-timeout=3.0.1=py_1000
- - attrs=20.3.0=pyhd3deb0d_0
- - ca-certificates=2020.11.8=h5b45459_0
- - certifi=2020.11.8=py37h03978a9_0
- - chardet=3.0.4=py37hf50a25e_1008
- - idna=2.10=pyh9f0ad1d_0
- - multidict=4.7.5=py37h4ab8f01_2
- - nest-asyncio=1.4.3=pyhd8ed1ab_0
- - openssl=1.1.1h=he774522_0
- - pip=20.2.4=py_0
- - python=3.7.8=h7840368_2_cpython
- - python_abi=3.7=1_cp37m
- - setuptools=49.6.0=py37hf50a25e_2
- - sqlite=3.33.0=he774522_1
- - strict-rfc3339=0.7=py_1
- - typing-extensions=3.7.4.3=0
- - typing_extensions=3.7.4.3=py_0
- - vc=14.1=h869be7e_1
- - vs2015_runtime=14.16.27012=h30e32a0_2
- - wheel=0.35.1=pyh9f0ad1d_0
- - wincertstore=0.2=py37hc8dfbb8_1005
- - yarl=1.6.3=py37hcc03f2d_0
- - zlib=1.2.11=h62dcd97_1010
- - pip:
- - docstring-parser==0.7.3
- - paho-mqtt==1.5.1
\ No newline at end of file
diff --git a/packages.png b/packages.png
new file mode 100644
index 0000000000000000000000000000000000000000..1279a094f75924331b205846ae375c32b83d8512
Binary files /dev/null and b/packages.png differ
diff --git a/requirements.txt b/requirements.txt
index d504927dc9f35d68b9ff8aa18b28dc626181c025..944862904ad4db43aa0b8517e25d352265507c2c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,5 @@
-aiohttp==3.7.3
-async-timeout==3.0.1
-attrs @ file:///home/conda/feedstock_root/build_artifacts/attrs_1605083924122/work
-certifi==2020.11.8
-chardet @ file:///D:/bld/chardet_1602255574834/work
-docstring-parser==0.7.3
-nest-asyncio==1.4.3
+fastapi==0.75.0
+toml==0.10.2
+fastapi-utils==0.2.1
+wzl-utilites==1.2.2
strict-rfc3339==0.7
-wzl-mqtt==2.3.0
-wzl-utilities==1.2.2
-yarl @ file:///D:/bld/yarl_1605429655746/work
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 25fa7e0c1e51fc4a49df2a8fe52d96e5b373b617..0000000000000000000000000000000000000000
--- a/setup.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from setuptools import setup, find_packages
-
-setup(name='wzl-udi',
- version='6.0.2',
- url='',
- author='Matthias Bodenbenner',
- author_email='m.bodenbenner@wzl.rwth-aachen.de',
- description='Provides REST-server, publisher-interface and serializer for the Unified Device Interface in Python.',
- package_dir={'wzl': 'src'},
- packages=['wzl.http', 'wzl.soil', 'wzl.utils'],
- long_description=open('./README.md').read(),
- install_requires=['nest-asyncio', 'strict-rfc3339', 'docstring_parser==0.7.1', 'aiohttp', 'wzl-mqtt', 'wzl-utilities', 'deprecated'],
- zip_safe=False)
diff --git a/src/http/__init__.py b/src/http/__init__.py
index 3f49305b8fd0a9fba599bbcade8c137fe32d6e94..8d1bf30a5fc420d2eccb5734dfe15bcd8bd01b01 100644
--- a/src/http/__init__.py
+++ b/src/http/__init__.py
@@ -1 +1 @@
-from .server import HTTPServer
\ No newline at end of file
+from .server_old import HTTPServer
\ No newline at end of file
diff --git a/src/http/app.py b/src/http/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d57cc76812678ca67e4563235028b6566f34d38
--- /dev/null
+++ b/src/http/app.py
@@ -0,0 +1,33 @@
+import io
+import json
+import os
+import traceback
+
+import toml
+from fastapi import FastAPI, Request, HTTPException
+from fastapi import File
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi_utils.inferring_router import InferringRouter
+from wzl.utilities import root_logger
+
+from http.server import ROUTER
+
+logger = root_logger.get(__name__)
+
+CONFIGURATION = toml.load(open('../config.toml'))
+
+app = FastAPI(root_path=CONFIGURATION['env']['root_path'])
+
+origins = [
+ "*",
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(ROUTER)
diff --git a/src/http/server.py b/src/http/server.py
index 5468b66d3acd49f274073d0a95f260d8d1cae8e2..e4230cf9f49022d1db2f79ac0beb57da1ca18b5f 100644
--- a/src/http/server.py
+++ b/src/http/server.py
@@ -1,46 +1,51 @@
# -*- coding: utf-8 -*-
import functools
+from typing import List
+
+from fastapi import Depends, FastAPI
+from fastapi_utils.cbv import cbv
+from fastapi_utils.inferring_router import InferringRouter
-from aiohttp import web
-from aiohttp.web import middleware
from wzl.utilities import root_logger
from .error import ServerException
from ..soil.error import InvokationException, ReadOnlyException, ChildNotFoundException
from ..utils.error import DeviceException, UserException
from ..soil.function import Function
-from ..soil.figure import Figure
+from ..soil.variable import Variable
from ..soil.object import Object
from ..soil.component import Component
from ..soil.parameter import Parameter
-from ..utils.constants import BASE_UUID_PATTERN, HTTP_GET, HTTP_OPTIONS
+from ..utils.const import BASE_UUID_PATTERN, HTTP_GET, HTTP_OPTIONS
logger = root_logger.get(__name__)
-
-@middleware
-async def cors(request, handler):
- logger.info("CORS Middleware handles request from {}".format(request.url))
- logger.debug('Request Headers: {}'.format(request.headers))
- response = web.Response()
- # check if the request is a preflight request
- if 'Access-Control-Request-Method' in request.headers and request.headers['Access-Control-Request-Method'] in ["POST", "PATCH"]:
- response.headers.update({'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
- 'Access-Control-Allow-Headers': request.headers['Access-Control-Request-Headers']})
- response.headers.update({'Access-Control-Allow-Origin': request.headers['Origin']})
- logger.debug('Preflight Response Headers :{}'.format(response.headers))
- return response
- try:
- response = await handler(request)
- except Exception as e:
- response = web.json_response({"description": str(e)}, status=500)
- logger.error(["[CORS] {} at {}".format(str(e), request.url)])
- finally:
- response.headers.update({'Access-Control-Allow-Origin': request.headers.get('Origin', "*")})
- logger.debug('Response Headers :{}'.format(response.headers))
- return response
-
-
+ROUTER = InferringRouter()
+
+
+# @middleware
+# async def cors(request, handler):
+# logger.info("CORS Middleware handles request from {}".format(request.url))
+# logger.debug('Request Headers: {}'.format(request.headers))
+# response = web.Response()
+# # check if the request is a preflight request
+# if 'Access-Control-Request-Method' in request.headers and request.headers['Access-Control-Request-Method'] in ["POST", "PATCH"]:
+# response.headers.update({'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
+# 'Access-Control-Allow-Headers': request.headers['Access-Control-Request-Headers']})
+# response.headers.update({'Access-Control-Allow-Origin': request.headers['Origin']})
+# logger.debug('Preflight Response Headers :{}'.format(response.headers))
+# return response
+# try:
+# response = await handler(request)
+# except Exception as e:
+# response = web.json_response({"description": str(e)}, status=500)
+# logger.error(["[CORS] {} at {}".format(str(e), request.url)])
+# finally:
+# response.headers.update({'Access-Control-Allow-Origin': request.headers.get('Origin', "*")})
+# logger.debug('Response Headers :{}'.format(response.headers))
+# return response
+
+@cbv(ROUTER)
class HTTPServer(object):
def __init__(self, loop, host, port, model):
@@ -49,37 +54,34 @@ class HTTPServer(object):
self.port = port
self.root = model
- self.app = web.Application(loop=self.loop, middlewares=[cors])
-
# define two routes for each request to make the 'objects' part optional
- self.app.router.add_get(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.get)
- self.app.router.add_get(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.get)
- self.app.router.add_post(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.post)
- self.app.router.add_post(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.post)
- self.app.router.add_delete(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.delete)
- self.app.router.add_delete(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.delete)
- self.app.router.add_route('OPTIONS', r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.options)
- self.app.router.add_route('OPTIONS', r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.options)
- self.app.router.add_put(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.put)
- self.app.router.add_put(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.put)
- self.app.router.add_patch(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.patch)
- self.app.router.add_patch(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.patch)
- web.run_app(self.app, host=self.host, port=self.port)
+ self.app.ROUTER.add_get(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.get)
+ self.app.ROUTER.add_get(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.get)
+ self.app.ROUTER.add_post(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.post)
+ self.app.ROUTER.add_post(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.post)
+ self.app.ROUTER.add_delete(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.delete)
+ self.app.ROUTER.add_delete(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.delete)
+ self.app.ROUTER.add_route('OPTIONS', r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.options)
+ self.app.ROUTER.add_route('OPTIONS', r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.options)
+ self.app.ROUTER.add_put(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.put)
+ self.app.ROUTER.add_put(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.put)
+ self.app.ROUTER.add_patch(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.patch)
+ self.app.ROUTER.add_patch(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.patch)
logger.info('HTTP-Server serving on {}:{}'.format(host, port))
@staticmethod
- def parse_uuids(request):
- uuids = request.match_info.get('uuids', 'uuids')
+ def parse_uuids(uuids: str) -> List[str]:
uuid_list = uuids.split('/')
while '' in uuid_list:
uuid_list.remove('')
return uuid_list
- async def get(self, request):
+ @ROUTER.get(r"/{uuids:(/' + BASE_UUID_PATTERN + r')*/?}")
+ async def get(self, request, uuids: str):
logger.info("GET Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
logger.debug('Query Parameters: {}'.format(request.query_string))
- item = self.root[HTTPServer.parse_uuids(request)]
+ item = self.root.get_element(HTTPServer.parse_uuids(uuids))
if request.query_string == "":
keys = []
else:
@@ -94,6 +96,7 @@ class HTTPServer(object):
logger.error('Response: {}'.format(response))
return web.json_response(response, status=status)
+ @ROUTER.post("/")
async def post(self, request):
logger.info("POST Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
@@ -117,6 +120,7 @@ class HTTPServer(object):
return web.json_response(response, status=status)
+ @ROUTER.delete("/")
async def delete(self, request):
logger.info("PUT Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
@@ -127,7 +131,8 @@ class HTTPServer(object):
if not isinstance(item, Object) and not isinstance(item, Component):
return web.json_response({}, status=405)
try:
- response = await self.loop.run_in_executor(None, functools.partial(item.remove, uuids[-1], *data['args'], **data['kwargs']))
+ response = await self.loop.run_in_executor(None, functools.partial(item.remove, uuids[-1], *data['args'],
+ **data['kwargs']))
status = 200
logger.info('Response: {}'.format(response))
except ChildNotFoundException as e:
@@ -140,12 +145,13 @@ class HTTPServer(object):
logger.error('Response: {}'.format(response))
return web.json_response(response, status=status)
+ @ROUTER.options("/")
async def options(self, request):
logger.info("HEAD Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
logger.debug('Query Parameters: {}'.format(request.query_string))
item = self.root[HTTPServer.parse_uuids(request)]
- if not isinstance(item, Figure):
+ if not isinstance(item, Variable):
return web.json_response({}, status=405)
if request.query_string == "":
keys = []
@@ -155,6 +161,7 @@ class HTTPServer(object):
logger.info('Response: {}'.format(response))
return web.json_response(response)
+ @ROUTER.patch("/")
async def patch(self, request):
logger.info("PATCH Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
@@ -180,6 +187,7 @@ class HTTPServer(object):
logger.error('Response: {}'.format(response))
return web.json_response(response, status=status)
+ @ROUTER.put("/")
async def put(self, request):
logger.info("PUT Request from {}".format(request.url))
logger.debug('Request: {}'.format(request))
@@ -191,7 +199,8 @@ class HTTPServer(object):
return web.json_response({}, status=405)
try:
response = await self.loop.run_in_executor(None,
- functools.partial(item.add, uuids[-1], data['class_name'], data['json_file'], *data['args'],
+ functools.partial(item.add, uuids[-1], data['class_name'],
+ data['json_file'], *data['args'],
**data['kwargs']))
status = 200
logger.info('Response: {}'.format(response))
diff --git a/src/http/server_old.py b/src/http/server_old.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea6ec99b6c0c1fc8875931dee381da5f9198db2a
--- /dev/null
+++ b/src/http/server_old.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+import functools
+
+from aiohttp import web
+from aiohttp.web import middleware
+from wzl.utilities import root_logger
+
+from .error import ServerException
+from ..soil.error import InvokationException, ReadOnlyException, ChildNotFoundException
+from ..utils.error import DeviceException, UserException
+from ..soil.function import Function
+from ..soil.variable import Variable
+from ..soil.object import Object
+from ..soil.component import Component
+from ..soil.parameter import Parameter
+from ..utils.const import BASE_UUID_PATTERN, HTTP_GET, HTTP_OPTIONS
+
+logger = root_logger.get(__name__)
+
+
+@middleware
+async def cors(request, handler):
+ logger.info("CORS Middleware handles request from {}".format(request.url))
+ logger.debug('Request Headers: {}'.format(request.headers))
+ response = web.Response()
+ # check if the request is a preflight request
+ if 'Access-Control-Request-Method' in request.headers and request.headers['Access-Control-Request-Method'] in ["POST", "PATCH"]:
+ response.headers.update({'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
+ 'Access-Control-Allow-Headers': request.headers['Access-Control-Request-Headers']})
+ response.headers.update({'Access-Control-Allow-Origin': request.headers['Origin']})
+ logger.debug('Preflight Response Headers :{}'.format(response.headers))
+ return response
+ try:
+ response = await handler(request)
+ except Exception as e:
+ response = web.json_response({"description": str(e)}, status=500)
+ logger.error(["[CORS] {} at {}".format(str(e), request.url)])
+ finally:
+ response.headers.update({'Access-Control-Allow-Origin': request.headers.get('Origin', "*")})
+ logger.debug('Response Headers :{}'.format(response.headers))
+ return response
+
+
+class HTTPServer(object):
+
+ def __init__(self, loop, host, port, model):
+ self.loop = loop
+ self.host = host
+ self.port = port
+ self.root = model
+
+ self.app = web.Application(loop=self.loop, middlewares=[cors])
+
+ # define two routes for each request to make the 'objects' part optional
+ self.app.ROUTER.add_get(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.get)
+ self.app.ROUTER.add_get(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.get)
+ self.app.ROUTER.add_post(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.post)
+ self.app.ROUTER.add_post(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.post)
+ self.app.ROUTER.add_delete(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.delete)
+ self.app.ROUTER.add_delete(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.delete)
+ self.app.ROUTER.add_route('OPTIONS', r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.options)
+ self.app.ROUTER.add_route('OPTIONS', r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.options)
+ self.app.ROUTER.add_put(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.put)
+ self.app.ROUTER.add_put(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.put)
+ self.app.ROUTER.add_patch(r'/objects{uuids:(/' + BASE_UUID_PATTERN + r')*/?}', self.patch)
+ self.app.ROUTER.add_patch(r'/{uuids:(' + BASE_UUID_PATTERN + r'($|/))*}', self.patch)
+ web.run_app(self.app, host=self.host, port=self.port)
+ logger.info('HTTP-Server serving on {}:{}'.format(host, port))
+
+ @staticmethod
+ def parse_uuids(request):
+ uuids = request.match_info.get('uuids', 'uuids')
+ uuid_list = uuids.split('/')
+ while '' in uuid_list:
+ uuid_list.remove('')
+ return uuid_list
+
+ async def get(self, request):
+ logger.info("GET Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ logger.debug('Query Parameters: {}'.format(request.query_string))
+ item = self.root[HTTPServer.parse_uuids(request)]
+ if request.query_string == "":
+ keys = []
+ else:
+ keys = [x.split('=')[0] for x in request.query_string.split('&')]
+ try:
+ response = item.serialize(keys, HTTP_GET)
+ status = 200
+ logger.info('Response: {}'.format(response))
+ except (DeviceException, ServerException, UserException) as e:
+ response = {'error': str(e)}
+ status = 500
+ logger.error('Response: {}'.format(response))
+ return web.json_response(response, status=status)
+
+ async def post(self, request):
+ logger.info("POST Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ data = await request.json()
+ logger.debug('Body: {}'.format(data))
+ uuids = HTTPServer.parse_uuids(request)
+ item = self.root[uuids]
+
+ if isinstance(item, Function):
+ try:
+ response = await item.invoke(data["arguments"], topic='/'.join(uuids))
+ status = 200
+ logger.info('Response: {}'.format(response))
+ except (DeviceException, ServerException, UserException) as e:
+ response = {'error': str(e)}
+ status = 500
+ logger.error('Response: {}'.format(response))
+ else:
+ response, status = {}, 405
+ logger.error('Response: {}'.format(response))
+
+ return web.json_response(response, status=status)
+
+ async def delete(self, request):
+ logger.info("PUT Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ data = await request.json()
+ uuids = HTTPServer.parse_uuids(request)
+ item = self.root[uuids[:-1]]
+
+ if not isinstance(item, Object) and not isinstance(item, Component):
+ return web.json_response({}, status=405)
+ try:
+ response = await self.loop.run_in_executor(None, functools.partial(item.remove, uuids[-1], *data['args'], **data['kwargs']))
+ status = 200
+ logger.info('Response: {}'.format(response))
+ except ChildNotFoundException as e:
+ response = {'error': str(e)}
+ status = 404
+ logger.error('Response: {}'.format(response))
+ except (DeviceException, ServerException, UserException) as e:
+ response = {'error': str(e)}
+ status = 500
+ logger.error('Response: {}'.format(response))
+ return web.json_response(response, status=status)
+
+ async def options(self, request):
+ logger.info("HEAD Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ logger.debug('Query Parameters: {}'.format(request.query_string))
+ item = self.root[HTTPServer.parse_uuids(request)]
+ if not isinstance(item, Variable):
+ return web.json_response({}, status=405)
+ if request.query_string == "":
+ keys = []
+ else:
+ keys = [x.split('=')[0] for x in request.query_string.split('&')]
+ response = item.serialize(keys, HTTP_OPTIONS)
+ logger.info('Response: {}'.format(response))
+ return web.json_response(response)
+
+ async def patch(self, request):
+ logger.info("PATCH Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ data = await request.json()
+ logger.debug('Body: {}'.format(data))
+ item = self.root[HTTPServer.parse_uuids(request)]
+
+ if isinstance(item, Parameter):
+ try:
+ response = await self.loop.run_in_executor(None, item.set, data["value"])
+ status = 200
+ logger.info('Response: {}'.format(response))
+ except ReadOnlyException as e:
+ response = {'error': str(e)}
+ status = 403
+ logger.error('Response: {}'.format(response))
+ except InvokationException as e:
+ response = {'error': str(e)}
+ status = 500
+ logger.error('Response: {}'.format(response))
+ else:
+ response, status = {}, 405
+ logger.error('Response: {}'.format(response))
+ return web.json_response(response, status=status)
+
+ async def put(self, request):
+ logger.info("PUT Request from {}".format(request.url))
+ logger.debug('Request: {}'.format(request))
+ data = await request.json()
+ logger.debug('Body: {}'.format(data))
+ uuids = HTTPServer.parse_uuids(request)
+ item = self.root[uuids[:-1]]
+ if not isinstance(item, Object) and not isinstance(item, Component):
+ return web.json_response({}, status=405)
+ try:
+ response = await self.loop.run_in_executor(None,
+ functools.partial(item.add, uuids[-1], data['class_name'], data['json_file'], *data['args'],
+ **data['kwargs']))
+ status = 200
+ logger.info('Response: {}'.format(response))
+ except (DeviceException, ServerException, UserException) as e:
+ response = {'error': str(e)}
+ status = 500
+ logger.error('Response: {}'.format(response))
+ return web.json_response(response, status=status)
diff --git a/src/soil/component.py b/src/soil/component.py
index dc6418df7015f34e3006e32fe3cda7256e030a33..6b910a6dd9e2df36a08c37736467321e320c4336 100644
--- a/src/soil/component.py
+++ b/src/soil/component.py
@@ -13,13 +13,14 @@ import sys
from typing import List, Any, Union, Dict
from wzl.utilities import root_logger
-from . import docstring_parser
+from .device import Device
from .element import Element
-from .error import ChildNotFoundException
+from .error import ElementNotExistException
from .function import Function
from .measurement import Measurement
from .parameter import Parameter
-from ..utils.constants import HTTP_GET
+from .stream import Job
+from ..utils.const import HTTP_GET
from ..utils.error import SerialisationException, DeviceException, UserException
logger = root_logger.get(__name__)
@@ -27,8 +28,9 @@ logger = root_logger.get(__name__)
class Component(Element):
- def __init__(self, uuid: str, name: str, description: str, functions: List[Function], measurements: List[Measurement],
- parameters: List[Parameter], components: List['Component'], mapping: Dict, ontology: str = None):
+ def __init__(self, uuid: str, name: str, description: str, functions: List[Function] = None,
+ measurements: List[Measurement] = None, parameters: List[Parameter] = None,
+ components: List['Component'] = None, ontology: str = None, device: Device = None):
"""
Args:
@@ -40,21 +42,6 @@ class Component(Element):
measurements: List of all children measurements.
parameters: List of all children parameters.
components: List of all children components. Might contain dynamic-components.
- mapping: Dictionary containing a mapping of the underlying device implementation to the HTTP-endpoints.
- The mapping of a component looks as follows :
-
- {
- 'getter': com_implementation.get,
- 'setter': com_implementation.set,
- 'MEA-Temperature': com_implementation.get_mea_temperature,
- 'PAR-State': {...},
- 'FUN-Reset: {...},
- 'COM-Part': {...},
- }
-
- If the component does not have dynamic children components, 'getter' and 'setter' are set to None.
- For all children there is a key-value pair where the UUID of the child is the key and the mapping of the child is the value.
- For the structure of the childrens' mappings please refer to the respective documentation.
ontology: Optional field containing the reference to a semantic definition of the components name or purpose.
Raises:
@@ -63,36 +50,93 @@ class Component(Element):
InvalidModelException: One of the lists containing the components' children is not a list or contains elements which are not of the correct type.
InvalidMappingException: If something is wrong with the provided mapping.
"""
- Element.__init__(self, uuid, name, description, ontology)
+ Element.__init__(self, uuid, name, description, ontology, device)
if uuid[:3] != 'COM':
- raise Exception('{}: The UUID must start with COM!'.format(uuid))
- if not isinstance(functions, list):
- raise Exception('{}: Given functions are not a list!'.format(uuid))
- for f in functions:
+ raise Exception(f'{uuid}: The UUID of component must start with COM!')
+
+ self._components = [] if components is None else components
+ if not isinstance(self._components, list):
+ raise Exception(f'{uuid}: Given components are not a list!')
+ for c in self._components:
+ if not isinstance(c, Component):
+ raise Exception(f'{uuid}: Given component is not of type Components!')
+
+ self._functions = [] if functions is None else functions
+ if not isinstance(self._functions, list):
+ raise Exception(f'{uuid}: Given functions are not a list!')
+ for f in self._functions:
if not isinstance(f, Function):
- raise Exception('{}: Given function is not of type Function!'.format(uuid))
- if not isinstance(measurements, list):
- raise Exception('{}: Given measurements are not a list!'.format(uuid))
- for v in measurements:
- if not isinstance(v, Measurement):
- raise Exception('{}: Given measurement is not of type Variables!'.format(uuid))
- if not isinstance(parameters, list):
- raise Exception('{}: Given measurements are not a list!'.format(uuid))
- for p in parameters:
+ raise Exception(f'{uuid}: Given function is not of type Function!')
+
+ self._measurements = [] if measurements is None else measurements
+ if not isinstance(self._measurements, list):
+ raise Exception(f'{uuid}: Given measurements are not a list!')
+ for m in self._measurements:
+ if not isinstance(m, Measurement):
+ raise Exception(f'{uuid}: Given measurement is not of type Variables!')
+
+ self._parameters = [] if parameters is None else parameters
+ if not isinstance(self._parameters, list):
+ raise Exception(f'{uuid}: Given measurements are not a list!')
+ for p in self._parameters:
if not isinstance(p, Parameter):
- raise Exception('{}: Given measurement is not of type Variables!'.format(uuid))
- if not isinstance(components, list):
- raise Exception('{}: Given components are not a list!'.format(uuid))
- for o in components:
- if not isinstance(o, Component):
- raise Exception('{}: Given component is not of type Components!'.format(uuid))
-
- self._functions = functions
- self._measurements = measurements
- self._components = components
- self._parameters = parameters
- self._implementation_add = implementation['add'] if 'add' in implementation else None
- self._implementation_remove = implementation['remove'] if 'remove' in implementation else None
+ raise Exception(f'{uuid}: Given measurement is not of type Variables!')
+
+ @property
+ def components(self) -> List['Component']:
+ return self._components
+
+ @property
+ def functions(self) -> List[Function]:
+ return self._functions
+
+ @property
+ def measurements(self) -> List[Measurement]:
+ return self._measurements
+
+ @property
+ def parameters(self) -> List[Parameter]:
+ return self._parameters
+
+ @property
+ def children(self) -> List[Element]:
+ return self.components + self.functions + self.measurements + self.parameters
+
+ @property
+ def jobs(self) -> List[Job]:
+ jobs = []
+ for child in self._measurements + self._parameters:
+ jobs += child.jobs
+ return jobs
+
+ def get_element(self, fqid: Union[str, List[str]]) -> Element:
+ """Goes down the component and tree and searches for the element with the given fqid.
+
+ If the fqid contains only one entry (i.e. a single uuid), it must be identical to the uuid of the component, the method is invoked of.
+
+ Args:
+ fqid: Unique identifier.
+
+ Returns:
+ The element having the specified fqid.
+ """
+ if not fqid:
+ raise ValueError(f'The fqid must not be empty!')
+
+ if isinstance(fqid, str):
+ fqid = fqid.split('/')
+
+ if fqid[0] == self.uuid:
+ if len(fqid) == 1:
+ return self
+ else:
+ for child in self.children:
+ try:
+ return child.get_element(fqid[1:])
+ except ElementNotExistException as e:
+ continue
+
+ raise ElementNotExistException(f'An element with the uuid {"/".join(fqid)} does not exist.')
def __getitem__(self, item: Union[str, List[str]], method: int = HTTP_GET) -> Any:
"""Returns the value of the specified item.
@@ -142,7 +186,8 @@ class Component(Element):
return o
else:
return child.__getitem__(item[1:], method)
- raise ChildNotFoundException(f'{self.uuid}: Given uuid {item} is not the id of a child of the current component!')
+ raise ChildNotFoundException(
+ f'{self.uuid}: Given uuid {item} is not the id of a child of the current component!')
return super().__getitem__(item, method)
def __setitem__(self, key: str, value: Any):
@@ -228,9 +273,11 @@ class Component(Element):
if 'description' in function:
component_list[idx]['description'] = function['description']
if 'arguments' in function:
- component_list[idx]['arguments'] = merge_measurements(function['arguments'], component_list[idx]['arguments'])
+ component_list[idx]['arguments'] = merge_measurements(function['arguments'],
+ component_list[idx]['arguments'])
if 'returns' in function:
- component_list[idx]['returns'] = merge_measurements(function['returns'], component_list[idx]['returns'])
+ component_list[idx]['returns'] = merge_measurements(function['returns'],
+ component_list[idx]['returns'])
return component_list
# merge components, i.e. overwrite fields of 'static' children dictionary with the 'dynamic' fields of the parents dictionary
@@ -241,7 +288,8 @@ class Component(Element):
if 'description' in parent_dict:
component_dict['description'] = parent_dict['description']
if 'measurements' in parent_dict:
- component_dict['measurements'] = merge_measurements(parent_dict['measurements'], component_dict['measurements'])
+ component_dict['measurements'] = merge_measurements(parent_dict['measurements'],
+ component_dict['measurements'])
if 'parameters' in parent_dict:
component_dict['paramters'] = merge_measurements(parent_dict['parameters'], component_dict['parameters'])
if 'functions' in parent_dict:
@@ -252,7 +300,8 @@ class Component(Element):
if len(index) != 1:
raise Exception('Mismatching UUID: {}.'.format(obj['uuid']))
index = index[0]
- component_dict['components'][index] = Component.merge_dictionaries(obj, component_dict['components'][index])
+ component_dict['components'][index] = Component.merge_dictionaries(obj,
+ component_dict['components'][index])
return component_dict
@staticmethod
@@ -262,26 +311,34 @@ class Component(Element):
uuid = dictionary['uuid']
if uuid[:3] != 'COM':
raise SerialisationException(
- 'The component can not be deserialized. The UUID must start with COM, but actually starts with {}!'.format(uuid[:3]))
+ 'The component can not be deserialized. The UUID must start with COM, but actually starts with {}!'.format(
+ uuid[:3]))
if 'file' in dictionary:
try:
with open(os.path.normpath(os.path.join(filepath, dictionary['file']))) as file:
component_dict = json.load(file)
dictionary = Component.merge_dictionaries(dictionary, component_dict)
except Exception as e:
- raise SerialisationException('{}: The component can not be deserialized. Provided JSON-file can not be parsed! {}'.format(uuid, e))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. Provided JSON-file can not be parsed! {}'.format(uuid,
+ e))
if 'name' not in dictionary:
raise SerialisationException('{}: The component can not be deserialized. Name is missing!'.format(uuid))
if 'description' not in dictionary:
- raise SerialisationException('{}: The component can not be deserialized. Description is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. Description is missing!'.format(uuid))
if 'measurements' not in dictionary:
- raise SerialisationException('{}: The component can not be deserialized. List of measurements is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. List of measurements is missing!'.format(uuid))
if 'parameters' not in dictionary:
- raise SerialisationException('{}: The component can not be deserialized. List of parameters is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. List of parameters is missing!'.format(uuid))
if 'functions' not in dictionary:
- raise SerialisationException('{}: The component can not be deserialized. List of functions is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. List of functions is missing!'.format(uuid))
if 'components' not in dictionary:
- raise SerialisationException('{}: The component can not be deserialized. List of components is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The component can not be deserialized. List of components is missing!'.format(uuid))
try:
measurements = []
@@ -292,7 +349,8 @@ class Component(Element):
else:
measurements += [Measurement.deserialize(var)]
except Exception as e:
- raise SerialisationException('{}: A measurement of the component can not be deserialized. {}'.format(uuid, e))
+ raise SerialisationException(
+ '{}: A measurement of the component can not be deserialized. {}'.format(uuid, e))
try:
parameters = []
for par in dictionary['parameters']:
@@ -322,10 +380,12 @@ class Component(Element):
else:
components += [Component.deserialize(obj, filepath=filepath)]
except Exception as e:
- raise SerialisationException('{}: An component of the component can not be deserialized. {}'.format(uuid, e))
+ raise SerialisationException(
+ '{}: An component of the component can not be deserialized. {}'.format(uuid, e))
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Component(dictionary['uuid'], dictionary['name'], dictionary['description'], functions, measurements, parameters, components, mapping,
+ return Component(dictionary['uuid'], dictionary['name'], dictionary['description'], functions, measurements,
+ parameters, components, mapping,
ontology)
except Exception as e:
raise SerialisationException('{}: The component can not be deserialized. {}'.format(uuid, e))
diff --git a/src/soil/datatype.py b/src/soil/datatype.py
new file mode 100644
index 0000000000000000000000000000000000000000..98f6422a0602120f23d7d77a0e7bd7184b381b49
--- /dev/null
+++ b/src/soil/datatype.py
@@ -0,0 +1,25 @@
+import enum
+
+
+class Datatype(enum.Enum):
+ BOOL = 0
+ INT = 1
+ FLOAT = 2
+ STRING = 3
+ ENUM = 4
+ TIME = 5
+
+ def __str__(self):
+ return Datatype.all()[self.value]
+
+ @staticmethod
+ def all():
+ return ['bool', 'int', 'float', 'string', 'enum', 'time']
+
+ @staticmethod
+ def from_string(name):
+ match = [i for i, x in enumerate(['bool', 'int', 'float', 'string', 'enum', 'time']) if x == name]
+ if len(match) == 1:
+ return Datatype(match[0])
+ else:
+ raise Exception('There is no datatype with this name!')
diff --git a/src/soil/device.py b/src/soil/device.py
new file mode 100644
index 0000000000000000000000000000000000000000..f47c3bd7052c2f922e6efb565bf012d5cf20d27c
--- /dev/null
+++ b/src/soil/device.py
@@ -0,0 +1,9 @@
+class Device(object):
+
+ def __init__(self):
+ # TODO implement
+ pass
+
+ def __del__(self):
+ # TODO implement
+ pass
\ No newline at end of file
diff --git a/src/soil/docstring_parser.py b/src/soil/docstring_parser.py
deleted file mode 100644
index b776036427483789379559e2a5c3880e357145d3..0000000000000000000000000000000000000000
--- a/src/soil/docstring_parser.py
+++ /dev/null
@@ -1,145 +0,0 @@
-from deprecated import deprecated
-import docstring_parser
-import types
-from typing import Any, Callable
-from wzl.utilities import root_logger
-
-from .stream import ConfigurableJob, FixedJob
-from ..utils.error import SerialisationException
-
-logger = root_logger.get(__name__)
-
-
-@deprecated(version='6.0.0', reason='Building service model from docstrings is too error-prone.')
-def parse_children(parse_docstrings: Callable, parent_url: str, implementation: Any):
- export_dict = {}
- children_attribute_list, uuid_attribute_list = [], []
-
- # parse docstring parameters
- doc = docstring_parser.parse(implementation.__doc__)
- try:
- uuid = doc.short_description.split('.')[0]
- if uuid[:3] != 'OBJ':
- raise SerialisationException('Short description of class {} misses UUID of the Object.'.format(implementation.__class__.__name__))
- except:
- raise SerialisationException('Short description of the class {} is missing.'.format(implementation.__class__.__name__))
-
- if len(doc.params) > 0:
- for element in doc.params:
- if isinstance(element, docstring_parser.common.DocstringParam) and element.args[1] == 'uuids':
- uuid_attribute_list = element.description.replace('\t', '').replace(' ', '').split(',')
- elif isinstance(element, docstring_parser.common.DocstringParam) and element.args[1] == 'children':
- children_attribute_list = element.description.replace('\t', '').replace(' ', '').split(',')
-
- # check if the uuid and children list parameters are matching
- if len(uuid_attribute_list) != len(children_attribute_list):
- raise SerialisationException(
- 'The number of attributes of uuids and and corresponding children differ!\nlen(uuids) != len(children): {} != {}'.format(
- len(uuid_attribute_list), len(children_attribute_list)))
-
- # try to retrieve attribute from implementation and parse the children
- for uuid_attribute, children_attribute in zip(uuid_attribute_list, children_attribute_list):
- try:
- uuid_list = implementation.__getattribute__(uuid_attribute)
- children_list = implementation.__getattribute__(children_attribute)
- for child_uuid, child in zip(uuid_list, children_list):
- child_url = '/'.join([uuid, child_uuid]) if parent_url == '' else '/'.join([parent_url, uuid, child_uuid])
- export_dict[child_uuid] = parse_docstrings(child, child_url)
- except AttributeError as e:
- raise SerialisationException('AttributeError: {}'.format(str(e)))
-
- return export_dict
-
-
-@deprecated(version='6.0.0', reason='Building service model from docstrings is too error-prone.')
-def parse_docstrings_for_mqtt(implementation, parent_url="", *args, **kwargs):
- if implementation is None:
- return None
-
- schedule = []
- child_schedule = parse_children(parse_docstrings_for_mqtt, parent_url, implementation)
- for key in child_schedule:
- schedule += [*child_schedule[key]]
- print(schedule)
-
- # parse functions and setters/getters of variables and parameters
- object_dict = implementation.__class__.__dict__
- funcs = [object_dict[x] for x in object_dict if isinstance(object_dict[x], types.FunctionType)]
- for func in funcs:
- doc = docstring_parser.parse(func.__doc__)
- if func.__name__ == '__init__' or doc.short_description == 'PUT.' or doc.short_description == 'DELETE.':
- continue
- short = doc.short_description
- if short is not None and short[:3] != 'FUN' and len(doc.params) == 0:
- returns = doc.returns.description.split('.')[0].split(',')
- assert (len(returns) == 1)
- if returns[0][:3] == 'VAR':
- lines = doc.long_description.split('\n')
- for line in lines:
- if line[:4] == 'MQTT':
- interval = line.replace(' ', '').split(':')[1]
- if len(interval) > 4 and interval[:4] == 'self':
- interval = implementation.__getattribute__(interval.split('.')[1])
- schedule += [
- ConfigurableJob('{}/{}'.format(parent_url, returns[0]), interval, implementation.__getattribute__(func.__name__))]
- else:
- schedule += [
- FixedJob('{}/{}'.format(parent_url, returns[0]), eval(interval), implementation.__getattribute__(func.__name__))]
- return schedule
-
-
-@deprecated(version='6.0.0', reason='Building service model from docstrings is too error-prone.')
-def parse_docstrings_for_soil(implementation: Any, parent_url="", *args, **kwargs):
- # TODO replace assertions by reasonable error handling
- if implementation is None:
- return None
-
- export_dict = parse_children(parse_docstrings_for_soil, parent_url, implementation)
-
- # parse functions and setters/getters of variables and parameters
- object_dict = implementation.__class__.__dict__
- funcs = [object_dict[x] for x in object_dict if isinstance(object_dict[x], types.FunctionType)]
- for func in funcs:
- try:
- if func.__name__[0] == '_' or func.__doc__ is None:
- continue
- doc = docstring_parser.parse(func.__doc__)
- short = doc.short_description
- if short is not None and short.split('-')[0] == 'FUN':
- d = {'method': implementation.__getattribute__(func.__name__), 'signature': {'arguments': {}, 'returns': []}}
- for arg in doc.params:
- desc = arg.description.split('.')
- d['signature']['arguments'][desc[0]] = arg.arg_name
- if doc.returns is not None:
- returns_string = doc.returns.description.split('.')[0]
- if ', ' in returns_string:
- d['signature']['returns'] = returns_string.split(', ')
- if ',\n' in returns_string:
- d['signature']['returns'] = returns_string.split(',\n')
- else:
- d['signature']['returns'] = returns_string.split(',')
- export_dict[short.split('.')[0]] = d
- elif short is not None and short.split('.')[0] == 'PUT':
- export_dict['add'] = implementation.__getattribute__(func.__name__)
- elif short is not None and short.split('.')[0] == 'DELETE':
- export_dict['remove'] = implementation.__getattribute__(func.__name__)
- elif (short is None or short[:3] != 'FUN') and len(doc.params) == 0 and doc.returns is not None:
- returns = doc.returns.description.split('.')[0].split(',')
- assert (len(returns) == 1)
- if returns[0][:3] == 'VAR':
- export_dict[returns[0]] = implementation.__getattribute__(func.__name__)
- else:
- if returns[0] not in export_dict:
- export_dict[returns[0]] = {'setter': None, 'getter': None}
- export_dict[returns[0]]['getter'] = implementation.__getattribute__(func.__name__)
- elif len(list(doc.params)) == 1:
- # assert (doc.returns.description.split('.')[0] == 'None')
- arg = doc.params[0].description.split('.')[0]
- assert (arg[:3] == 'PAR')
- if arg not in export_dict:
- export_dict[arg] = {'setter': None, 'getter': None}
- export_dict[arg]['setter'] = implementation.__getattribute__(func.__name__)
- except SerialisationException as e:
- logger.error("{}: Error in docstring parsing: {}".format(func.__name__, e))
-
- return export_dict
diff --git a/src/soil/element.py b/src/soil/element.py
index 323180f1aff28670b8f87ae5f7004b973332feac..db584cee11781db746383639eb1793309f2702e3 100644
--- a/src/soil/element.py
+++ b/src/soil/element.py
@@ -1,68 +1,97 @@
from abc import abstractmethod, ABC
import re
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Union
from wzl.utilities import root_logger
-from ..utils.constants import BASE_UUID_PATTERN, HTTP_GET
+from .device import Device
+from .error import ElementNotExistException
+from ..utils.const import BASE_UUID_PATTERN, HTTP_GET
logger = root_logger.get(__name__)
class Element(ABC):
+ """SOIL base class. All objects of a SOIL model are of type Element.
+
+ """
UUID_PATTERN = re.compile(BASE_UUID_PATTERN)
- def __init__(self, uuid: str, name: str, description: str, ontology: str = None):
+ def __init__(self, uuid: str, name: str, description: str, ontology: str = None, device: Device = None):
+ """Constructor
+
+ Args:
+ uuid: Locally unique identifier of the element.
+ name: Human readable name of the element.
+ description: Brief description of the element to give users a short explanation of the purpose or meaning of the element.
+ ontology: URI of a formally defined term from an established ontology or controlled vocabulary.
+ """
if not isinstance(name, str) or name == '':
- raise Exception('{}: Name is no string or the empty string!'.format(uuid))
+ raise Exception(f'{uuid}: Name is no string or the empty string!')
if not isinstance(description, str) or description == '':
- raise Exception('{}: Description is no string or the empty string!'.format(uuid))
+ raise Exception(f'{uuid}: Description is no string or the empty string!')
if ontology is not None and not isinstance(ontology, str):
- raise Exception('{}: Onthology is no string!'.format(uuid))
+ raise Exception(f'{uuid}: Ontology is no string!')
if not isinstance(uuid, str) or not Element.UUID_PATTERN.match(uuid):
- raise Exception('Cannot use uuid {}. Wrong format!'.format(uuid))
+ raise Exception(f'Cannot use uuid {uuid}. Wrong format!')
else:
self._uuid = uuid
self._name = name
self._description = description
self._ontology = ontology
+ self._device = device
@property
- def uuid(self):
+ def uuid(self) -> str:
return self._uuid
- def __getitem__(self, item: str, method: int = HTTP_GET) -> Any:
- if item == "uuid":
- return self._uuid
- if item == "name":
- return self._name
- if item == "description":
- return self._description
- if item == "ontology":
- return self._ontology
- raise Exception("{}: Key error. No attribute is named '{}'".format(self.uuid, item))
+ @property
+ def name(self) -> str:
+ return self._name
+
+ @property
+ def description(self) -> str:
+ return self._description
+
+ @property
+ def ontology(self) -> str:
+ return self._description
+
+ @abstractmethod
+ def get_element(self, fqid: Union[str, List[str]]) -> 'Element':
+ """Goes down the component and tree and searches for the element with the given fqid.
+
+ If the fqid contains only one entry (i.e. a single uuid), it must be identical to the uuid of the component, the method is invoked of.
+
+ Args:
+ fqid: Unique identifier.
+
+ Returns:
+ The element having the specified fqid.
+ """
+ ...
+
+ def __getitem__(self, item: str) -> Any:
+ if item == 'uuid':
+ return self.uuid
+ if item == 'name':
+ return self.name
+ if item == 'description':
+ return self.description
+ if item == 'ontology':
+ return self.ontology
+ raise Exception(f'{self.uuid}: Key error. No attribute is named "{item}"')
def __setitem__(self, key: str, value: Any):
- if key == "name":
- if not isinstance(value, str) or value == '':
- raise Exception('{}: Name is no string or the empty string!'.format(self.uuid))
- self._name = value
- elif key == "description":
- if not isinstance(value, str) or value == '':
- raise Exception('{}: Description is no string or the empty string!'.format(self.uuid))
- self._description = value
- elif key == "ontology":
- if value is not None and not isinstance(value, str):
- raise Exception('{}: Ontology is no string!'.format(self.uuid))
- self._ontology = value
+ if key in ['name', 'description', 'ontology', 'uuid']:
+ raise Exception(f'{self.uuid}: The {key} can not be changed during runtime.')
else:
- raise Exception(
- "{}: Key error. No attribute is named '{}' or it should not be changed".format(self.uuid, key))
+ raise KeyError(f'{self.uuid}: No attribute is named "{key}".')
- def serialize(self, keys: List[str], method: int = HTTP_GET) -> Dict:
+ def serialize(self, keys: List[str]) -> Dict[str, Any]:
res = {'uuid': self._uuid}
for key in keys:
- res[key] = self.__getitem__(key, method)
+ res[key] = self.__getitem__(key)
if not keys: # list is empty => serialize complete component
res['name'] = self._name
res['description'] = self._description
diff --git a/src/soil/error.py b/src/soil/error.py
index 173189b1f71787daf140547a5a9c0d48981b417a..a6582b5f6865866fe763b2e0e1fcb1ab92c93ddd 100644
--- a/src/soil/error.py
+++ b/src/soil/error.py
@@ -24,7 +24,7 @@ class ChildNotFound(UserException):
UserException.__init__(self, description)
-class ChildNotFoundException(DeviceException):
+class ElementNotExistException(DeviceException):
def __init__(self, description):
BasicException.__init__(self, description)
diff --git a/src/soil/event.py b/src/soil/event.py
index 7ab51bb53037e68d6dc9b7b23070be1696a2b154..a5af3cca647fdf0c6dcd2dfd0d619c573e877965 100644
--- a/src/soil/event.py
+++ b/src/soil/event.py
@@ -24,7 +24,7 @@ class EventSeverity(IntEnum):
class EventTrigger(Flag):
"""Specifies under which conditions the associated event is triggered.
- Flags can be logically combined. Not all combinations of cause are meaningful.
+ Flags can be logically combined. Not all combinations are meaningful, of cause.
"""
NOT = auto()
EQUALS = auto()
diff --git a/src/soil/figure.py b/src/soil/figure.py
deleted file mode 100644
index b885cf0b0ae1c4cab1346403fe3ad941d9f89d47..0000000000000000000000000000000000000000
--- a/src/soil/figure.py
+++ /dev/null
@@ -1,299 +0,0 @@
-from abc import ABC
-
-import asyncio
-import inspect
-
-import datetime
-import nest_asyncio
-import strict_rfc3339 as rfc3339
-import time
-from typing import Any, List, Callable, Union
-from wzl.utilities import root_logger
-
-nest_asyncio.apply()
-
-from .element import Element
-from .error import DimensionException, RangeException, TypeException, NotImplementedException
-from ..utils.constants import HTTP_GET, HTTP_OPTIONS
-from ..utils.error import DeviceException
-
-logger = root_logger.get(__name__)
-
-
-def parse_time(time_rfc3339: Union[str, List]):
- if isinstance(time_rfc3339, list):
- return [parse_time(e) for e in time_rfc3339]
- else:
- if time_rfc3339 is None or time_rfc3339 == "":
- return None
- timestamp = rfc3339.rfc3339_to_timestamp(time_rfc3339)
- date = list(time.gmtime(int(timestamp)))[:6]
- return datetime.datetime(*date, int((timestamp - int(timestamp)) * 1e6))
-
-
-def serialize_time(time):
- timestamp = datetime.datetime.timestamp(time)
- return rfc3339.timestamp_to_rfc3339_localoffset(timestamp)
-
-
-class Figure(Element, ABC):
- def __init__(self, uuid: str, name: str, description: str, datatype: str, dimension: List, range: List, value: Any, getter: Callable,
- ontology: str = None):
- Element.__init__(self, uuid, name, description, ontology)
- if type(datatype) is not str:
- raise Exception('{}: Datatype must be passed as string.'.format(uuid))
- Figure.check_all(datatype, dimension, range, value)
- if getter is not None and not callable(getter):
- raise TypeError("{}: The getter of the Figure must be callable!".format(uuid))
- self._datatype = datatype
- self._dimension = dimension
- self._range = range
- if datatype == 'time':
- self._value = parse_time(value)
- else:
- self._value = value
- self._getter = getter
-
-
- @property
- def datatype(self):
- return self._datatype
-
- @property
- def dimension(self):
- return self._dimension
-
- @property
- def range(self):
- return self._range
-
- def __getitem__(self, item: str, method=HTTP_GET):
- """
- Getter-Method.
- According to the given key the method returns the value of the corresponding attribute.
- :param item: name of the attribute. Provided as string without leading underscores.
- :param method: ???
- :return: the value of the attribute indicated by 'item'.
- """
- if item == "datatype":
- return self._datatype
- if item == "value":
- if method != HTTP_OPTIONS:
- try:
- if inspect.iscoroutinefunction(self.get):
- loop = asyncio.get_event_loop()
- value = loop.run_until_complete(asyncio.gather(self.get()))[0]
- else:
- value = self.get()
-
- if self._datatype == 'time':
- value = serialize_time(value)
- elif self._datatype == 'enum':
- value = str(value)
-
- except Exception as e:
- raise DeviceException('Could not provide value of Measurement/Parameter {}: {}'.format(self.uuid, str(e)), predecessor=e)
-
- Figure.check_all(self._datatype, self._dimension, self._range, value)
- return value
- else:
- return self._value
- if item == 'dimension':
- return self._dimension
- if item == 'range':
- return self._range
- if item == []:
- return self
- return super().__getitem__(item, method)
-
- # def __setitem__(self, key: str, value):
- # """
- # Setter - Method
- # If key is "value" datatype, dimension and range is checked for correctness.
- # :param key: sets the value of the attribute with name 'item' to the provided value.
- # :param value: value to be set
- # """
- # super().__setitem__(key, value)
-
- def serialize(self, keys: [str], method=HTTP_GET):
- """
- Serializes an object of type Figure into a JSON-like dictionary.
- :param keys: All attributes given in the "keys" array are serialized.
- :param method: ???
- :return: a dictionary having all "keys" as keys and the values of the corresponding attributes as value.
- """
- # list is empty provide all attributes of the default-serialization
- if not keys:
- keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range']
- # get all attribute values
- dictionary = {}
- for key in keys:
- value = self.__getitem__(key, method)
- dictionary[key] = value
- return dictionary
-
- @staticmethod
- def check_dimension(dimension: List, value: Any):
- """
- Checks whether the given value is of given dimension
- :param dimension: the dimension the value provided by "value" should have
- :param value: value to be checked for the dimension
- """
- # dimension of undefined value must not be checked => valid
- if value is None:
- return
- # base case 1: dimension is empty and variable is not a scalar => not valid
- if not dimension and not Figure.is_scalar(value):
- raise DimensionException('Figure of dimension 0 can not be of type list!')
- # base case 2: dimension is empty and variable is a scalar => valid
- elif not dimension:
- return
- try:
- # base case 3: current dimension is fixed size "x" and length of the value is not "x" => not valid
- if dimension[0] != 0 and len(value) != dimension[0]:
- raise DimensionException('Dimension of data does not match dimension of variable!')
- except TypeError as te:
- raise DimensionException(str(te))
- # recursion case
- # at this point value is guaranteed to be of type list
- # => recursively check the dimension of each "subvalue"
- for v in value:
- try:
- Figure.check_dimension(dimension[1:], v)
- except DimensionException as e:
- raise e
-
- @staticmethod
- def check_type(datatype, value):
- """
- Checks if the given value is of the correct datatype. If value is not a scale, it checks all "subvalues" for correct datatype.
- :param datatype: datatype the value provided by "value" should have
- :param value: value to be checked for correct datatype
- """
- # datatype of undefined value must not be checked => valid
- if value is None:
- return
- # base case: value is a scalar
- if Figure.is_scalar(value):
- # check if the type of value corresponds to given datatype
- if datatype == 'bool' and not isinstance(value, bool):
- raise TypeException("Boolean field does not match non-boolean value {}!".format(value))
- elif datatype == 'int' and not isinstance(value, int):
- raise TypeException("Integer field does not match non-integer value {}!".format(value))
- elif datatype == 'double' and not isinstance(value, float) and not isinstance(value, int):
- raise TypeException("Double field does not match non-double value {}!".format(value))
- elif datatype == 'string' and not isinstance(value, str):
- raise TypeException("String field does not match non-string value {}!".format(value))
- elif datatype == 'enum' and not isinstance(value, str):
- raise TypeException(
- "Enum field {} must be a string!".format(value))
- elif datatype == 'time' and not isinstance(value, str):
- raise TypeException(
- "Time field {} must be string.".format(
- value))
- elif datatype == 'time' and isinstance(value, str):
- if value != "" and value is not None and not rfc3339.validate_rfc3339(value):
- raise TypeException("Value is not a valid RFC3339-formatted timestring: {}".format(value))
- elif datatype not in ["bool", "int", "double", "string", "enum", "time"]:
- raise TypeException("Unknown type descriptor: {}".format(datatype))
- else:
- # recursion case: value is an array or matrix => check datatype of each "subvalue" recursively
- for v in value:
- try:
- Figure.check_type(datatype, v)
- except TypeException as e:
- raise e
-
- @staticmethod
- def check_range(datatype, range, value):
- """
- Checks if the given value is within provided range (depending on the given datatype)
-
- IMPORTANT: It is not checked whether the value is of correct type. If the type of value is not correct, the result
- of check_range is not meaningful! To get expressive result check datatype before calling check_range!
-
- :param datatype: datatype of the value
- :param range: the range the value should be within
- :param value: value to be checked for range
-
- For all datatypes (except "bool" and "enum")the range specification is of the following form: [lower bound (LB), upper bound (UB)]
- If LB or UB are None the value is unrestricted to the bottom or top, respectively.
- In case of "int", "double" and "time" the interpretation of LB and UB is straightforward.
- For "string" LB and UB restrict the length of the string. (If LB is given as None, 0 is the natural LB of cause)
- "bool" is naturally bounded to "True" and "False", thus the range is not checked.
- In case of "enum" the range contains the list of all possible values.
- """
- # if the list is empty, all values are possible
- if not range:
- if datatype == 'enum':
- raise RangeException('A value of type enum must provide a range with possible values!')
- else:
- return
- # base case: value is scalar => check if the value is in range
- if Figure.is_scalar(value):
- # bool is not checked, since there is only true and false
- if datatype == 'bool':
- return
- elif datatype == 'int' and value is not None:
- if range[0] is not None and value < range[0]:
- raise RangeException("Integer value {} is smaller than lower bound {}!".format(value, range[0]))
- elif range[1] is not None and value > range[1]:
- raise RangeException("Integer value {} is higher than upper bound {}!".format(value, range[1]))
- elif datatype == 'double' and value is not None:
- if range[0] is not None and value < range[0]:
- raise RangeException("Double value {} is smaller than lower bound {}!".format(value, range[0]))
- elif range[1] is not None and value > range[1]:
- raise RangeException("Double value {} is higher than upper bound {}!".format(value, range[1]))
- elif datatype == 'string' and value is not None:
- if range[0] is not None and len(value) < range[0]:
- raise RangeException(
- "String value {} is too short. Minimal required length is {}!".format(value, range[0]))
- elif range[1] is not None and len(value) > range[1]:
- raise RangeException(
- "String value {} is too long. Maximal allowed length is {}!".format(value, range[1]))
- elif datatype == 'enum' and value is not None:
- if value not in range:
- raise RangeException("Enum value {} is not within the set of allowed values!".format(value))
- elif datatype == 'time' and value is not None and value != "":
- if range[0] is not None:
- if not rfc3339.validate_rfc3339(range[0]):
- raise TypeException(
- "Can not check range of time value. Lower bound {} is not a valid RFC3339 timestring.".format(
- range[0]))
- if parse_time(value) < parse_time(range[0]):
- raise RangeException(
- "Time value {} is smaller than lower bound {}!".format(parse_time(value),
- parse_time(range[0])))
- elif range[1] is not None:
- if not rfc3339.validate_rfc3339(range[1]):
- raise TypeException(
- "Can not check range of time value. Upper bound {} is not a valid RFC3339 timestring.".format(
- range[0]))
- if parse_time(value) > parse_time(range[1]):
- raise RangeException(
- "Time value {} is greater than upper bound {}!".format(parse_time(value),
- parse_time(range[1])))
- else:
- # recursion case: value is an array or matrix => check range of each "subvalue" recursively
- for v in value:
- try:
- Figure.check_range(datatype, range, v)
- except RangeException as e:
- raise e
-
- @staticmethod
- def is_scalar(value):
- return not isinstance(value, list)
-
- @staticmethod
- def check_all(datatype, dimension, range, value):
- Figure.check_type(datatype, value)
- Figure.check_dimension(dimension, value)
- Figure.check_range(datatype, range, value)
-
- @property
- def get(self):
- if self._getter is not None:
- return self._getter
- else:
- raise NotImplementedException(self._uuid, self._name)
diff --git a/src/soil/function.py b/src/soil/function.py
index b4e42cd08ec50814b12bbe480306f582615ba8b7..2dab6b6c0242f7ce479c914cfd6971c70a317427 100644
--- a/src/soil/function.py
+++ b/src/soil/function.py
@@ -1,46 +1,93 @@
+import abc
import functools
import inspect
-from typing import Any, Dict, List, Union
+from typing import Any, Dict, List, Union, Callable
from wzl.utilities import root_logger
+from device.device import Device
from .element import Element
-from .error import InvokationException, NotImplementedException
+from .error import InvokationException, NotImplementedException, ElementNotExistException
from ..utils.error import SerialisationException, DeviceException
-from .figure import Figure
+from .variable import Variable
from .parameter import Parameter
-from ..utils.constants import HTTP_GET, HTTP_OPTIONS
+from ..utils.const import HTTP_GET, HTTP_OPTIONS
logger = root_logger.get(__name__)
-class Function(Element):
+def raise_publish_exception(uuid: str, name: str):
+ raise InvokationException(uuid, name)
- def __init__(self, uuid: str, name: str, description: str, arguments: List[Figure], returns: List[Figure],
- implementation: Dict, ontology: str = None):
- Element.__init__(self, uuid, name, description, ontology)
+
+class Function(Element, abc.ABC):
+
+ def __init__(self, uuid: str, name: str, description: str, arguments: List[Parameter] = None,
+ returns: List[Parameter] = None, ontology: str = None, publish: Callable = None, device: Device = None):
+ Element.__init__(self, uuid, name, description, ontology, device)
if uuid[:3] != 'FUN':
- raise Exception('{}: The UUID must start with FUN!'.format(uuid))
+ raise Exception(f'{uuid}: The UUID must start with FUN!')
+
+ self._arguments = [] if arguments is None else arguments
if not isinstance(arguments, list):
- raise Exception('{}: Given arguments are not a list!'.format(uuid))
- for i in arguments:
- if not isinstance(i, Parameter):
- raise Exception('{}: Given argument is not of type Parameter!'.format(uuid))
+ raise Exception(f'{uuid}: Given arguments are not a list!')
+ for a in arguments:
+ if not isinstance(a, Parameter):
+ raise Exception(f'{uuid}: Given argument is not of type Parameter!')
+
+ self._returns = [] if returns is None else returns
if not isinstance(returns, list):
- raise Exception('{}: Given returns are not a list!'.format(uuid))
- for o in returns:
- if not isinstance(o, Parameter):
- raise Exception('{}: Given return is not of type Parameter!'.format(uuid))
-
- self._arguments = arguments
- self._returns = returns
- self._implementation = implementation['method']
- self._signature = implementation['signature']
- self._mqtt_callback = implementation['mqtt_callback'] if 'mqtt_callback' in implementation.keys() else None
-
- def __getitem__(self, item: Union[str, List[str]], method: int = HTTP_GET) -> Any:
- if item == "arguments":
+ raise Exception(f'{uuid}: Given returns are not a list!')
+ for r in returns:
+ if not isinstance(r, Parameter):
+ raise Exception(f'{uuid}: Given return is not of type Parameter!')
+
+ self._publish = raise_publish_exception if publish is None else publish
+
+ @property
+ def arguments(self) -> List[Variable]:
+ return self._arguments
+
+ @property
+ def returns(self) -> List[Variable]:
+ return self._returns
+
+ @property
+ def children(self) -> List[Element]:
+ return self.arguments + self.returns
+
+ def get_element(self, fqid: Union[str, List[str]]) -> Element:
+ """Goes down the component and tree and searches for the element with the given fqid.
+
+ If the fqid contains only one entry (i.e. a single uuid), it must be identical to the uuid of the component, the method is invoked of.
+
+ Args:
+ fqid: Unique identifier.
+
+ Returns:
+ The element having the specified fqid.
+ """
+ if not fqid:
+ raise ValueError(f'The fqid must not be empty!')
+
+ if isinstance(fqid, str):
+ fqid = fqid.split('/')
+
+ if fqid[0] == self.uuid:
+ if len(fqid) == 1:
+ return self
+ else:
+ for child in self.children:
+ try:
+ return child.get_element(fqid[1:])
+ except ElementNotExistException as e:
+ continue
+
+ raise ElementNotExistException(f'An element with the uuid {"/".join(fqid)} does not exist.')
+
+ def __getitem__(self, item: Union[str, List[str]]) -> Any:
+ if item == 'arguments':
return self._arguments
- if item == "returns":
+ if item == 'returns':
return self._returns
if isinstance(item, list):
if len(item) == 0:
@@ -49,11 +96,12 @@ class Function(Element):
for o in everything:
if o.uuid == item[0]:
return o[item[1:]]
- raise Exception("{}: Given uuid {} is not the id of a child of the current component!".format(self.uuid, item))
- return super().__getitem__(item, method)
+ raise Exception(f'{self.uuid}: Given uuid {item} is not the id of a child of the current component!')
+ return super().__getitem__(item)
def __setitem__(self, key: str, value: Any):
- if key == "arguments":
+ # TODO refactor
+ if key == 'arguments':
if not isinstance(value, list):
raise Exception('{}: Given arguments are not a list!'.format(self.uuid))
if len(value) != len(self._arguments):
@@ -61,32 +109,34 @@ class Function(Element):
'{}: The number of given arguments does not match the number of required arguments!'.format(
self.uuid))
for v in value:
- if not isinstance(v, Figure):
+ if not isinstance(v, Variable):
raise Exception('{}: Given argument is not of type Figure!'.format(self.uuid))
self._arguments = value
- elif key == "returns":
+ elif key == 'returns':
if not isinstance(value, list):
raise Exception('{}: Given returns are not a list!'.format(self.uuid))
if len(value) != len(self._returns):
raise Exception(
'{}: The number of given returns does not match the number of required returns!'.format(self.uuid))
for v in value:
- if not isinstance(v, Figure):
+ if not isinstance(v, Variable):
raise Exception('{}: Given return is not of type Figure!'.format(self.uuid))
self._returns = value
else:
super().__setitem__(key, value)
- async def invoke(self, arguments: List[Figure], topic) -> Dict[str, List[Dict[str, Any]]]:
- returns = {"returns": []}
+ @abc.abstractmethod
+ def execute(self, *args, **kwargs):
+ ...
+
+ async def invoke(self, arguments: List[Variable], topic: str) -> List[Variable]:
+ returns = {'returns': []}
args = {}
- if self._implementation is None:
- raise NotImplementedException(self._uuid, self._name)
for a in arguments:
- var = self.__getitem__([a["uuid"]])
- Figure.check_all(var.datatype, var.dimension, var.range, a["value"])
- args[self._signature['arguments'][a["uuid"]]] = a["value"]
+ var = self.__getitem__([a['uuid']])
+ Variable.check_all(var.datatype, var.dimension, var.range, a['value'])
+ args[self._signature['arguments'][a['uuid']]] = a['value']
# set up servers
try:
@@ -109,17 +159,18 @@ class Function(Element):
result = (result,)
if len(result) != len(self._returns):
raise InvokationException(self._uuid, self._name,
- "Internal Server Error. Function with UUID {} should return {} parameters, but invoked method returned {} values!".format(
+ 'Internal Server Error. Function with UUID {} should return {} parameters, but invoked method returned {} values!'.format(
self.uuid, len(result), len(self._returns)))
for value, uuid in zip(result, self._signature['returns']):
var = [x for x in self._returns if x['uuid'] == uuid]
if len(var) != 1:
raise InvokationException(self._uuid, self._name,
- "Internal Server Error. UUID {} of returned parameter does not match!".format(uuid))
+ 'Internal Server Error. UUID {} of returned parameter does not match!'.format(
+ uuid))
else:
var = var[0]
- Figure.check_all(var.datatype, var.dimension, var.range, value)
+ Variable.check_all(var.datatype, var.dimension, var.range, value)
returns['returns'] += [{'uuid': uuid, 'value': value}]
return returns
@@ -130,11 +181,14 @@ class Function(Element):
dictionary = super().serialize(keys)
if 'arguments' in keys:
dictionary['arguments'] = list(
- map(lambda x: x.serialize(['name', 'uuid', 'description', 'datatype', 'value', 'dimension', 'range', 'ontology'], HTTP_OPTIONS),
+ map(lambda x: x.serialize(
+ ['name', 'uuid', 'description', 'datatype', 'value', 'dimension', 'range', 'ontology'],
+ HTTP_OPTIONS),
self._arguments))
if 'returns' in keys:
dictionary['returns'] = list(
- map(lambda x: x.serialize(['name', 'uuid', 'description', 'datatype', 'dimension', 'ontology'], HTTP_OPTIONS), self._returns))
+ map(lambda x: x.serialize(['name', 'uuid', 'description', 'datatype', 'dimension', 'ontology'],
+ HTTP_OPTIONS), self._returns))
return dictionary
@staticmethod
@@ -144,15 +198,19 @@ class Function(Element):
uuid = dictionary['uuid']
if uuid[:3] != 'FUN':
raise SerialisationException(
- 'The Function can not be deserialized. The UUID must start with FUN, but actually starts with {}!'.format(uuid[:3]))
+ 'The Function can not be deserialized. The UUID must start with FUN, but actually starts with {}!'.format(
+ uuid[:3]))
if 'name' not in dictionary:
raise SerialisationException('{}: The function can not be deserialized. Name is missing!'.format(uuid))
if 'description' not in dictionary:
- raise SerialisationException('{}: The function can not be deserialized. Description is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The function can not be deserialized. Description is missing!'.format(uuid))
if 'arguments' not in dictionary:
- raise SerialisationException('{}: The function can not be deserialized. List of arguments is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The function can not be deserialized. List of arguments is missing!'.format(uuid))
if 'returns' not in dictionary:
- raise SerialisationException('{}: The function can not be deserialized. List of returns is missing!'.format(uuid))
+ raise SerialisationException(
+ '{}: The function can not be deserialized. List of returns is missing!'.format(uuid))
try:
arguments = []
@@ -168,6 +226,7 @@ class Function(Element):
raise SerialisationException('{}: A return of the function can not be deserialized. {}'.format(uuid, e))
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Function(dictionary['uuid'], dictionary['name'], dictionary['description'], arguments, returns, implementation, ontology)
+ return Function(dictionary['uuid'], dictionary['name'], dictionary['description'], arguments, returns,
+ implementation, ontology)
except Exception as e:
raise SerialisationException('{}: The function can not be deserialized. {}'.format(uuid, e))
diff --git a/src/soil/measurement.py b/src/soil/measurement.py
index c38f79210e81eced6e27f6b63d673011e04af087..3707765e22843628579e4fda8af20e635183aabc 100644
--- a/src/soil/measurement.py
+++ b/src/soil/measurement.py
@@ -1,47 +1,47 @@
-from deprecated import deprecated
-from typing import Dict
+from typing import Dict, List, Any, Union
+
from wzl.utilities import root_logger
-from ..utils.constants import HTTP_GET
+from device.device import Device
+from .datatype import Datatype
+from .element import Element
+from .error import ElementNotExistException
+from .variable import Variable
+from ..utils.const import Range
from ..utils.error import SerialisationException
-from .figure import Figure
logger = root_logger.get(__name__)
-class Measurement(Figure):
+class Measurement(Variable):
- def __init__(self, uuid, name, description, datatype, dimension, range, getter, unit, nonce=None, ontology: str = None):
- Figure.__init__(self, uuid, name, description, datatype, dimension, range, None, getter, ontology)
+ def __init__(self, uuid: str, name: str, description: str, datatype: Datatype, dimension: List[int],
+ range: Range, unit: str = None, nonce=None, ontology: str = None, device: Device = None):
+ Variable.__init__(self, uuid, name, description, datatype, dimension, range, unit, ontology, device)
if uuid[:3] != 'MEA':
- raise Exception('{}: The UUID must start with MEA!'.format(uuid))
- self._unit = unit
+ raise Exception(f'{uuid}: The UUID of a measurement must start with MEA!')
self._covariance = None # TODO init
self._uncertainty = None # TODO init
self._timestamp = None
self._nonce = nonce
@property
- def unit(self):
- return self._unit
-
- @property
- def covariance(self):
+ def covariance(self) -> Any:
return self._covariance
@property
- def uncertainty(self):
+ def uncertainty(self) -> Any:
return self._uncertainty
@property
- def timestamp(self):
+ def timestamp(self) -> str:
return self._timestamp
@property
- def nonce(self):
+ def nonce(self) -> str:
return self._nonce
- def __getitem__(self, item: str, method=HTTP_GET):
+ def __getitem__(self, item: str):
"""
Getter-Method.
According to the given key the method returns the value of the corresponding attribute.
@@ -49,8 +49,6 @@ class Measurement(Figure):
:param method: ???
:return: the value of the attribute indicated by 'item'.
"""
- if item == "unit":
- return self._unit
if item == 'nonce':
return self._nonce
if item == 'covariance':
@@ -61,7 +59,7 @@ class Measurement(Figure):
return self._timestamp
if item == []:
return self
- return super().__getitem__(item, method)
+ return super().__getitem__(item)
def __setitem__(self, key: str, value):
"""
@@ -71,15 +69,13 @@ class Measurement(Figure):
:param value: value to be set
"""
if key in ['value', 'timestamp', 'covariance', 'uncertainty']:
- raise KeyError('The {} attribute of a measurement can not be set manually!'.format(key))
- elif key == "nonce":
+ raise KeyError(f'The {key} of a measurement can not be set manually!')
+ elif key == 'nonce':
self._nonce = self._nonce
- elif key == 'unit':
- self._unit = self._unit
else:
super().__setitem__(key, value)
- def serialize(self, keys: [str], method=HTTP_GET):
+ def serialize(self, keys: [str]):
"""
Serializes an object of type Measurement into a JSON-like dictionary.
:param keys: All attributes given in the "keys" array are serialized.
@@ -88,23 +84,26 @@ class Measurement(Figure):
"""
# list is empty provide all attributes of the default-serialization
if not keys:
- keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range', 'timestamp', 'nonce', 'covariance', 'uncertainty',
- 'unit', 'ontology']
- # TODO temporary workaround, forces the server to always return the timestamp if the value is queried
+ keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range', 'timestamp', 'nonce',
+ 'covariance', 'uncertainty', 'unit', 'ontology']
+
+ # if the value is returned, always return the timestamp!
if 'value' in keys and 'timestamp' not in keys:
keys += ['timestamp']
dictionary = {}
+
# get all attribute values
for key in keys:
- value = self.__getitem__(key, method)
+ value = self.__getitem__(key)
# in case of timestamp convert into RFC3339 string
+ # TODO fix time formatting
if key == 'timestamp' or (key == 'value' and self._datatype == 'time'):
- value = value.isoformat() + 'Z' if value is not None else ""
+ value = value.isoformat() + 'Z' if value is not None else ''
dictionary[key] = value
return dictionary
@staticmethod
- def deserialize(dictionary: Dict, implementation=None):
+ def deserialize(dictionary: Dict[str, Any]) -> 'Measurement':
"""
Takes a JSON-like dictionary, parses it, performs a complete correctness check and returns an object of type Figure with the
values provided in the dictionary, if dictionary is a valid serialization of a Figure.
@@ -112,30 +111,23 @@ class Measurement(Figure):
:param implementation: implementation wrapper object,
:return: an object of type Figure
"""
- # check if all required attributes are present
+
if 'uuid' not in dictionary:
raise SerialisationException('The measurement can not be deserialized. UUID is missing!')
uuid = dictionary['uuid']
if uuid[:3] != 'MEA':
raise SerialisationException(
- 'The Measurement can not be deserialized. The UUID must start with MEA, but actually starts with {}!'.format(uuid[:3]))
- if 'name' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Name is missing!'.format(uuid))
- if 'description' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Description is missing!'.format(uuid))
- if 'datatype' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Datatype is missing!'.format(uuid))
- if 'dimension' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Dimension is missing!'.format(uuid))
- if 'value' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Value is missing!'.format(uuid))
- if 'range' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Range is missing!'.format(uuid))
- if 'unit' not in dictionary:
- raise SerialisationException('{}: The measurement can not be deserialized. Unit is missing!'.format(uuid))
+ f'The Measurement can not be deserialized. The UUID must start with MEA, but actually starts with {uuid[:3]}!')
+
+ # check if all required attributes are present
+ for key in ['name', 'description', 'datatype', 'dimension', 'range']:
+ if key not in dictionary:
+ raise SerialisationException(f'{uuid}: The measurement can not be deserialized. {key.capitalize()} is missing!')
try:
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Measurement(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'], dictionary['dimension'],
- dictionary['range'], implementation, dictionary['unit'], ontology)
+ unit = dictionary['unit'] if 'unit' in dictionary else None
+ return Measurement(dictionary['uuid'], dictionary['name'], dictionary['description'],
+ dictionary['datatype'], dictionary['dimension'],
+ dictionary['range'], unit, ontology)
except Exception as e:
raise SerialisationException('{}: The measurement can not be deserialized. {}'.format(uuid, e))
diff --git a/src/soil/object.py b/src/soil/object.py
deleted file mode 100644
index ade708a9e5c64fd67d8370958a75588b486da8f5..0000000000000000000000000000000000000000
--- a/src/soil/object.py
+++ /dev/null
@@ -1,333 +0,0 @@
-# from __future__ import annotations
-import json
-import os
-import sys
-from deprecated import deprecated
-from typing import List, Any, Union, Dict
-from wzl.utilities import root_logger
-
-from . import docstring_parser
-from .element import Element
-from .error import ChildNotFoundException
-from ..utils.error import SerialisationException, DeviceException, UserException
-from .function import Function
-from .variable import Variable
-from .parameter import Parameter
-from ..utils.constants import HTTP_GET
-
-logger = root_logger.get(__name__)
-
-
-@deprecated(version='6.0.0', reason='"Object" has been renamed to "Component" to avoid ambiguity.')
-class Object(Element):
-
- def __init__(self, uuid: str, name: str, description: str, functions: List[Function], variables: List[Variable], parameters: List[Parameter],
- objects: List['Object'], implementation: Dict, ontology: str = None):
- Element.__init__(self, uuid, name, description, ontology)
- if not isinstance(functions, list):
- raise Exception('{}: Given functions are not a list!'.format(uuid))
- for f in functions:
- if not isinstance(f, Function):
- raise Exception('{}: Given function is not of type Function!'.format(uuid))
- if not isinstance(variables, list):
- raise Exception('{}: Given variables are not a list!'.format(uuid))
- for v in variables:
- if not isinstance(v, Variable):
- raise Exception('{}: Given variable is not of type Variables!'.format(uuid))
- if not isinstance(parameters, list):
- raise Exception('{}: Given variables are not a list!'.format(uuid))
- for p in parameters:
- if not isinstance(p, Parameter):
- raise Exception('{}: Given variable is not of type Variables!'.format(uuid))
- if not isinstance(objects, list):
- raise Exception('{}: Given objects are not a list!'.format(uuid))
- for o in objects:
- if not isinstance(o, Object):
- raise Exception('{}: Given object is not of type Objects!'.format(uuid))
-
- self._functions = functions
- self._variables = variables
- self._objects = objects
- self._parameters = parameters
- self._implementation_add = implementation['add'] if 'add' in implementation else None
- self._implementation_remove = implementation['remove'] if 'remove' in implementation else None
-
- def __getitem__(self, item: Union[str, List[str]], method: int = HTTP_GET) -> Any:
- attribute = False
- if isinstance(item, str):
- attribute = hasattr(self, item)
- if item == "functions":
- return self._functions
- if item == "variables":
- return self._variables
- if item == "parameters":
- return self._variables
- if item == "objects":
- return self._objects
- if item == "children":
- ret = []
- everything = self._objects + self._variables + self._parameters + self._functions
- for o in everything:
- ret += [o.uuid]
- return ret
- # if the item is a list, the list contains the uuid of the descendants
- if isinstance(item, list):
- if len(item) > 0 and super().__getitem__('uuid', method) == item[0]:
- item = item[1:]
- if len(item) == 0:
- return self
- everything = self._objects + self._variables + self._parameters + self._functions
- for o in everything:
- if o.uuid == item[0]:
- if len(item) == 1:
- return o
- else:
- return o.__getitem__(item[1:], method)
- raise Exception("{}: Given uuid {} is not the id of a child of the current object!".format(self.uuid, item))
- return super().__getitem__(item, method)
-
- def __setitem__(self, key: str, value: Any):
- if key == "functions":
- if not isinstance(value, list):
- raise Exception('{}: Given functions are not a list!'.format(self.uuid))
- for f in value:
- if not isinstance(f, Function):
- raise Exception('{}: Given function is not of type Function!'.format(self.uuid))
- self._functions = value
- elif key == "variables":
- if not isinstance(value, list):
- raise Exception('{}: Given variables are not a list!'.format(self.uuid))
- for v in value:
- if not isinstance(v, Variable):
- raise Exception('{}: Given variable is not of type Variable!'.format(self.uuid))
- self._variables = value
- elif key == "parameters":
- if not isinstance(value, list):
- raise Exception('{}: Given parameters are not a list!'.format(self.uuid))
- for v in value:
- if not isinstance(v, Parameter):
- raise Exception('{}: Given parameter is not of type Parameter!'.format(self.uuid))
- self._variables = value
- elif key == "objects":
- if not isinstance(value, list):
- raise Exception('{}: Given objects are not a list!'.format(self.uuid))
- for o in value:
- if not isinstance(o, Object):
- raise Exception('{}: Given object is not of type Object!'.format(self.uuid))
- self._objects = value
- else:
- super().__setitem__(key, value)
-
- def serialize(self, keys: List[Any], method: int = HTTP_GET) -> Dict[str, Any]:
- if not keys: # list is empty
- keys = ['uuid', 'name', 'description', 'children', 'ontology']
-
- if 'all' in keys: # serialize complete tree recursively (overrides all other keys)
- dictionary = super().serialize([])
- dictionary['variables'] = list(map(lambda x: x.serialize([]), self._variables))
- dictionary['functions'] = list(map(lambda x: x.serialize(['all']), self._functions))
- dictionary['objects'] = list(map(lambda x: x.serialize(['all']), self._objects))
- dictionary['parameters'] = list(map(lambda x: x.serialize(['all']), self._parameters))
- return dictionary
-
- dictionary = super().serialize(keys, method)
- if 'children' in keys:
- everything = self._objects + self._variables + self._parameters + self._functions
- dictionary['children'] = list(map(lambda x: x.serialize(['name', 'uuid']), everything))
- # if 'update' in keys:
- # # TODO implement (update all children)
- # pass
- # # if self._implementation is not None:
- # # dict['update'] = self._implementation()
- # else:
- # dict['children'] = list(map(lambda x: x.serialize(['value', 'uuid']), self._variables))
- return dictionary
-
- @staticmethod
- def merge_dictionaries(parent_dict, object_dict):
- def merge_variables(parent_list, object_list):
- for variable in parent_list:
- if 'uuid' not in variable:
- raise Exception('UUID {} not given for variable to be overwritten.'.format(variable['uuid']))
- idx = [i for i, v in enumerate(object_list) if v['uuid'] == variable['uuid']]
- if len(idx) != 1:
- raise Exception('Mismatching UUID: {}.'.format(variable['uuid']))
- idx = idx[0]
- object_list[idx].update(variable)
- return object_list
-
- def merge_functions(parent_list, object_list):
- for function in parent_list:
- if 'uuid' not in function:
- raise Exception('UUID {} not given for function to be overwritten.'.format(function['uuid']))
- idx = [i for i, v in enumerate(object_list) if v['uuid'] == function['uuid']]
- if len(idx) != 1:
- raise Exception('Mismatching UUID: {}.'.format(function['uuid']))
- idx = idx[0]
- if 'name' in function:
- object_list[idx]['name'] = function['name']
- if 'description' in function:
- object_list[idx]['description'] = function['description']
- if 'arguments' in function:
- object_list[idx]['arguments'] = merge_variables(function['arguments'], object_list[idx]['arguments'])
- if 'returns' in function:
- object_list[idx]['returns'] = merge_variables(function['returns'], object_list[idx]['returns'])
- return object_list
-
- # merge objects, i.e. overwrite fields of "static" children dictionary with the "dynamic" fields of the parents dictionary
- uuid = parent_dict['uuid']
- object_dict['uuid'] = uuid
- if 'name' in parent_dict:
- object_dict['name'] = parent_dict['name']
- if 'description' in parent_dict:
- object_dict['description'] = parent_dict['description']
- if 'variables' in parent_dict:
- object_dict['variables'] = merge_variables(parent_dict['variables'], object_dict['variables'])
- if 'parameters' in parent_dict:
- object_dict['paramters'] = merge_variables(parent_dict['parameters'], object_dict['parameters'])
- if 'functions' in parent_dict:
- object_dict['functions'] = merge_functions(parent_dict['functions'], object_dict['functions'])
- if 'objects' in parent_dict:
- for obj in parent_dict['objects']:
- index = [i for i, o in enumerate(object_dict['objects']) if o['uuid'] == obj['uuid']]
- if len(index) != 1:
- raise Exception('Mismatching UUID: {}.'.format(obj['uuid']))
- index = index[0]
- object_dict['objects'][index] = Object.merge_dictionaries(obj, object_dict['objects'][index])
- return object_dict
-
- @staticmethod
- def deserialize(dictionary, mapping=None, filepath=''):
- if 'uuid' not in dictionary:
- raise SerialisationException('The object can not be deserialized. UUID is missing!')
- uuid = dictionary['uuid']
- if 'file' in dictionary:
- try:
- with open(os.path.normpath(os.path.join(filepath, dictionary['file']))) as file:
- object_dict = json.load(file)
- dictionary = Object.merge_dictionaries(dictionary, object_dict)
- except Exception as e:
- raise SerialisationException('{}: The object can not be deserialized. Provided JSON-file can not be parsed! {}'.format(uuid, e))
- if 'name' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. Name is missing!'.format(uuid))
- if 'description' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. Description is missing!'.format(uuid))
- if 'variables' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. List of variables is missing!'.format(uuid))
- if 'parameters' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. List of parameters is missing!'.format(uuid))
- if 'functions' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. List of functions is missing!'.format(uuid))
- if 'objects' not in dictionary:
- raise SerialisationException('{}: The object can not be deserialized. List of objects is missing!'.format(uuid))
-
- try:
- variables = []
- for var in dictionary['variables']:
- if mapping is not None:
- submapping = mapping[var["uuid"]] if var['uuid'] in mapping else None
- variables += [Variable.deserialize(var,submapping)]
- else:
- variables += [Variable.deserialize(var)]
- except Exception as e:
- raise SerialisationException('{}: A variable of the object can not be deserialized. {}'.format(uuid, e))
- try:
- parameters = []
- for par in dictionary['parameters']:
- if mapping is not None:
- submapping = mapping[par["uuid"]] if par['uuid'] in mapping else None
- parameters += [Parameter.deserialize(par, submapping)]
- else:
- parameters += [Parameter.deserialize(par)]
- except Exception as e:
- raise SerialisationException('{}: A parameter of the object can not be deserialized. {}'.format(uuid, e))
- try:
- functions = []
- for func in dictionary['functions']:
- if mapping is not None:
- submapping = mapping[func["uuid"]] if func['uuid'] in mapping else None
- functions += [Function.deserialize(func, submapping)]
- else:
- functions += [Function.deserialize(func)]
- except Exception as e:
- raise SerialisationException('{}: A function of the object can not be deserialized. {}'.format(uuid, e))
- try:
- objects = []
- for obj in dictionary['objects']:
- if mapping is not None:
- submapping = mapping[obj["uuid"]] if obj['uuid'] in mapping else None
- objects += [Object.deserialize(obj, submapping, filepath)]
- else:
- objects += [Object.deserialize(obj, filepath=filepath)]
- except Exception as e:
- raise SerialisationException('{}: An object of the object can not be deserialized. {}'.format(uuid, e))
- try:
- ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Object(dictionary['uuid'], dictionary['name'], dictionary['description'], functions, variables, parameters, objects, mapping,
- ontology)
- except Exception as e:
- raise SerialisationException('{}: The object can not be deserialized. {}'.format(uuid, e))
-
- def write(self, filename: str):
- if filename[-5:] != ".json":
- raise Exception('{} is not a json file!'.format(filename))
-
- model_dict = self.serialize(['all'])
-
- f = open(filename, "w")
- f.write(json.dumps(model_dict))
- f.close()
-
- def update(self, element: Union['Object', Function, Variable, Parameter]):
- if isinstance(element, Object):
- for i, o in enumerate(self._objects):
- if o.uuid == element.uuid:
- self._objects[i] = element
- return
- # self._objects.append(element)
- else:
- raise Exception("Wrong type updating element on existing model!")
-
- def add(self, uuid: str, class_name: str, json_file: str, *args, **kwargs):
- if uuid[:3] == 'OBJ':
- if uuid not in [o.uuid for o in self._objects]:
- try:
- __import__(class_name)
- implementation = getattr(sys.modules[class_name], class_name)(*args, **kwargs)
- mapping = docstring_parser.parse_docstrings_for_soil(implementation)
- self._objects += [Object.load(json_file, mapping)]
- if self._implementation_add is not None:
- self._implementation_add(implementation)
- except Exception as e:
- raise DeviceException('Can not add object with UUID {}. {}'.format(uuid, e), predecessor=e)
- else:
- raise UserException('Object has already a child with UUID {}.'.format(uuid))
- else:
- raise UserException('UUID {} is not of the UUID of an object.'.format(uuid))
-
- def remove(self, uuid: str, *args, **kwargs):
- for o in self._objects:
- if o.uuid == uuid:
- if self._implementation_remove is not None:
- try:
- self._implementation_remove(*args, **kwargs)
- except Exception as e:
- raise DeviceException(str(e), predecessor=e)
- self._objects.remove(o)
- return
- raise ChildNotFoundException('{}: Child {} not found!'.format(self.uuid, uuid))
-
- @staticmethod
- def load(file: Union[str, dict], implementation: Any) -> 'Object':
- if isinstance(file, str):
- if not os.path.isfile(file):
- raise Exception('There is no file named {}!'.format(file))
- if file[-5:] != ".json":
- raise Exception('{} is not a json file!'.format(file))
- with open(file, 'r') as f:
- model_dict = json.load(f)
- return Object.deserialize(model_dict, implementation, os.path.dirname(file))
- elif isinstance(file, dict):
- return Object.deserialize(file, implementation)
- else:
- raise Exception('Given file is not a name of a json-file nor a json-like dictionary.')
\ No newline at end of file
diff --git a/src/soil/parameter.py b/src/soil/parameter.py
index 24f0f33e71d0b92fc109e74687e35c77fccd1ead..5ba81591dac95cc18539ae0be57e8ce9685b8096 100644
--- a/src/soil/parameter.py
+++ b/src/soil/parameter.py
@@ -1,47 +1,48 @@
import asyncio
import inspect
-from typing import Dict
+from typing import Dict, Any, List
from wzl.utilities import root_logger
-from ..utils.constants import HTTP_GET
+from .datatype import Datatype
+from .device import Device
+from ..utils.const import HTTP_GET, Range
from ..utils.error import DeviceException, SerialisationException
from .error import ReadOnlyException
-from .figure import Figure
+from .variable import Variable
logger = root_logger.get(__name__)
-class Parameter(Figure):
+class Parameter(Variable):
- def __init__(self, uuid, name, description, datatype, dimension, range, value, getter=None, setter=None, ontology: str = None):
- Figure.__init__(self, uuid, name, description, datatype, dimension, range, value, getter, ontology)
+ def __init__(self, uuid: str, name: str, description: str, datatype: Datatype, dimension: List[int], range: Range,
+ constant: bool, value: Any, unit: str = None, ontology: str = None, device: Device = None):
+ Variable.__init__(self, uuid, name, description, datatype, dimension, range, value, unit, ontology, device)
if uuid[:3] not in ['PAR', 'ARG', 'RET']:
- raise Exception('{}: The UUID must start with PAR, ARG or RET!'.format(uuid))
- if setter is not None and not callable(setter):
- raise TypeError("{}: The setter of the variable must be callable!".format(uuid))
- self._setter = setter
+ raise Exception(f'{uuid}: The UUID must start with PAR, ARG or RET!')
+ self._constant = constant
- def __setitem__(self, key: str, value):
+ @property
+ def constant(self) -> bool:
+ return self._constant
+
+ def set_value(self, value: Any):
+ self._value = value
+
+ def __setitem__(self, key: str, value: Any):
"""
Setter - Method
If key is "value" datatype, dimension and range is checked for correctness.
:param key: sets the value of the attribute with name 'item' to the provided value.
:param value: value to be set
"""
- if key == "value":
- Figure.check_all(self._datatype, self._dimension, self._range, value)
- # self._timestamp, value = self._implementation()
+ if key == 'value':
+ if self.constant:
+ raise Exception(f'{self.uuid}: The parameter is a constant. Value can not be changed!')
+
+ Variable.check_all(self._datatype, self._dimension, self._range, value)
try:
- if inspect.iscoroutinefunction(self.set):
- try:
- loop = asyncio.get_running_loop()
- except:
- loop = asyncio.get_event_loop()
- loop.run_until_complete(self.set(value))
- self._value = value
- else:
- self.set(value)
- self._value = value
+ self.set_value(value)
except Exception as e:
raise DeviceException(str(e), predecessor=e)
else:
@@ -91,32 +92,17 @@ class Parameter(Figure):
uuid = dictionary['uuid']
if uuid[:3] not in ['PAR', 'ARG', 'RET']:
raise SerialisationException(
- 'The Parameter can not be deserialized. The UUID must start with PAR, ARG or RET, but actually starts with {}!'.format(uuid[:3]))
- if 'name' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Name is missing!'.format(uuid))
- if 'description' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Description is missing!'.format(uuid))
- if 'datatype' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Datatype is missing!'.format(uuid))
- if 'dimension' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Dimension is missing!'.format(uuid))
- if 'value' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Value is missing!'.format(uuid))
- if 'range' not in dictionary:
- raise SerialisationException('{}: The parameter can not be deserialized. Range is missing!'.format(uuid))
+ f'The Parameter can not be deserialized. The UUID must start with PAR, ARG or RET, but actually starts with {uuid[:3]}!')
+
+ # check if all required attributes are present
+ for key in ['name', 'description', 'datatype', 'dimension', 'range', 'value']:
+ if key not in dictionary:
+ raise SerialisationException(f'{uuid}: The parameter can not be deserialized. {key.capitalize()} is missing!')
+
try:
- # create Parameter
- getter = implementation['getter'] if implementation is not None else None
- setter = implementation['setter'] if implementation is not None else None
ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Parameter(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'], dictionary['dimension'],
- dictionary['range'], dictionary['value'], getter, setter, ontology)
+ unit = dictionary['unit'] if 'unit' in dictionary else None
+ return Parameter(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'],
+ dictionary['dimension'], dictionary['range'], dictionary['value'], unit, ontology)
except Exception as e:
- raise SerialisationException('{}: The variable can not be deserialized. {}'.format(uuid, e))
-
- @property
- def set(self):
- if self._setter is not None:
- return self._setter
- else:
- raise ReadOnlyException(self._uuid, self._name)
+ raise SerialisationException(f'{uuid}: The parameter can not be deserialized. {e}')
diff --git a/src/soil/stream.py b/src/soil/stream.py
index c6064696a9912080c112a0d63de05f4a29f5a079..522b8bc391e2f5fbdd82450eacd0a043d3edbfb1 100644
--- a/src/soil/stream.py
+++ b/src/soil/stream.py
@@ -58,7 +58,7 @@ class FixedJob(Job):
return self._interval
-class ConfigurableJob(Job):
+class DynamicJob(Job):
"""
Works exactly as a Job, despite interval is a callable which returns an integer value, used for determining delay between two job executions.
"""
@@ -112,9 +112,24 @@ class StreamScheduler(ABC):
if start_immediately:
self._update()
- @abstractmethod
def _process_job(self, job):
- ...
+ if isinstance(job, UpdateJob):
+ if job.updated:
+ for publisher in self._publishers:
+ publisher.publish(job.topic, json.dumps({'uuid': job.topic, 'value': job.value}), 1)
+
+ elif isinstance(job, EventJob):
+ value = job.callback()
+ event = job.event
+ if event.is_triggered(value):
+ event.trigger(value)
+ for publisher in self._publishers:
+ publisher.publish('events/' + job.topic, json.dumps(event.serialize()), 1)
+
+ else:
+ ret = {'uuid': job.topic, 'value': job.callback()}
+ for publisher in self._publishers:
+ publisher.publish(job.topic, json.dumps(ret), 1)
def start(self):
self._running = True
@@ -158,38 +173,3 @@ class StreamScheduler(ABC):
self._loop.call_later((next - now).seconds + (next - now).microseconds / 1e6, self._update)
-
-class MessageScheduler(StreamScheduler):
-
- def __init__(self, loop, schedule: List[Job], publishers: List[MQTTPublisher] = None, start_immediately: bool = False):
- StreamScheduler.__init__(self, loop, schedule, publishers, start_immediately or len(schedule) > 0)
-
- def _process_job(self, job):
- ret = {'uuid': job.topic, 'value': job.callback()}
- for publisher in self._publishers:
- publisher.publish(job.topic, json.dumps(ret), 1)
-
-
-class EventScheduler(StreamScheduler):
-
- def __init__(self, loop, schedule: List[Job], publishers: List[MQTTPublisher] = None, start_immediately: bool = False):
- StreamScheduler.__init__(self, loop, schedule, publishers, start_immediately or len(schedule) > 0)
-
- def _process_job(self, job):
- value = job.callback()
- event = job.event
- if event.is_triggered(value):
- event.trigger(value)
- for publisher in self._publishers:
- publisher.publish('events/' + job.topic, json.dumps(event.serialize()), 1)
-
-
-class UpdateScheduler(StreamScheduler):
-
- def __init__(self, loop, schedule: List[UpdateJob], publishers: List[MQTTPublisher] = None, start_immediately: bool = False):
- StreamScheduler.__init__(self, loop, schedule, publishers, start_immediately or len(schedule) > 0)
-
- def _process_job(self, job: UpdateJob):
- if job.updated:
- for publisher in self._publishers:
- publisher.publish(job.topic, json.dumps({'uuid': job.topic, 'value': job.value}), 1)
diff --git a/src/soil/variable.py b/src/soil/variable.py
index 836796635bb2a972b953353f8cc17a5404d56eb7..01bf567df38f5e0aced15cec3b40a201b0f1ffe4 100644
--- a/src/soil/variable.py
+++ b/src/soil/variable.py
@@ -1,46 +1,104 @@
-from deprecated import deprecated
-from typing import Dict
+import datetime
+import enum
+import time
+from abc import ABC
+from typing import Any, List, Union, Dict, Tuple
+
+import strict_rfc3339 as rfc3339
from wzl.utilities import root_logger
-from ..utils.constants import HTTP_GET
-from ..utils.error import SerialisationException
-from .figure import Figure
+from utils.const import Range
+from .datatype import Datatype
+from .device import Device
+from .stream import Job
+
+
+from .element import Element
+from .error import DimensionException, RangeException, TypeException, ElementNotExistException
+from ..utils.error import DeviceException
logger = root_logger.get(__name__)
-@deprecated(version='6.0.0', reason='"Variable" has been renamed to "Measurement" to be consistent with VIM and published articles.')
-class Variable(Figure):
+def parse_time(time_rfc3339: Union[str, List]):
+ if isinstance(time_rfc3339, list):
+ return [parse_time(e) for e in time_rfc3339]
+ else:
+ if time_rfc3339 is None or time_rfc3339 == "":
+ return None
+ timestamp = rfc3339.rfc3339_to_timestamp(time_rfc3339)
+ date = list(time.gmtime(int(timestamp)))[:6]
+ return datetime.datetime(*date, int((timestamp - int(timestamp)) * 1e6))
+
+
+def serialize_time(time):
+ timestamp = datetime.datetime.timestamp(time)
+ return rfc3339.timestamp_to_rfc3339_localoffset(timestamp)
+
- def __init__(self, uuid, name, description, datatype, dimension, range, getter, unit, nonce=None, ontology: str = None):
- Figure.__init__(self, uuid, name, description, datatype, dimension, range, None, getter, ontology)
+class Variable(Element, ABC):
+ def __init__(self, uuid: str, name: str, description: str, datatype: Datatype, dimension: List, range: Range,
+ value: Any, unit: str = None, ontology: str = None, device: Device = None):
+ Element.__init__(self, uuid, name, description, ontology, device)
+ if not isinstance(datatype, Datatype):
+ raise Exception(f'{uuid} - InitializationError: Datatype must be an object of class Datatype.')
+ Variable.check_all(datatype, dimension, range, value)
+ self._datatype = datatype
+ self._dimension = dimension
+ self._range = range
+ if datatype in [Datatype.INT, Datatype.FLOAT] and unit is None:
+ raise Exception(
+ f'{uuid} - InitializationError: If the datatype is integer or float a unit must be specified!')
self._unit = unit
- self._covariance = None # TODO init
- self._uncertainty = None # TODO init
- self._timestamp = None
- self._nonce = nonce
+ if datatype == 'time':
+ self._value = parse_time(value)
+ else:
+ self._value = value
+
+ self._jobs: List[Job] = []
@property
- def unit(self):
- return self._unit
+ def datatype(self) -> Datatype:
+ return self._datatype
@property
- def covariance(self):
- return self._covariance
+ def dimension(self) -> List[int]:
+ return self._dimension
@property
- def uncertainty(self):
- return self._uncertainty
+ def range(self) -> Range:
+ return self._range
@property
- def timestamp(self):
- return self._timestamp
+ def unit(self) -> str:
+ return self._unit
@property
- def nonce(self):
- return self._nonce
+ def jobs(self) -> List[Job]:
+ return self.jobs
+
+ def get_value(self) -> Any:
+ return self._value
- def __getitem__(self, item: str, method=HTTP_GET):
+ def add_job(self, job: Job):
+ self._jobs += [job]
+
+ def remove_job(self, index: int):
+ del self._jobs[index]
+
+ def get_element(self, fqid: Union[str, List[str]]) -> Element:
+ if not fqid:
+ raise ValueError(f'The fqid must not be empty!')
+
+ if isinstance(fqid, str):
+ fqid = fqid.split('/')
+
+ if fqid[0] == self.uuid and len(fqid) == 1:
+ return self
+
+ raise ElementNotExistException(f'An element with the uuid {"/".join(fqid)} does not exist.')
+
+ def __getitem__(self, item: str) -> Any:
"""
Getter-Method.
According to the given key the method returns the value of the corresponding attribute.
@@ -48,90 +106,208 @@ class Variable(Figure):
:param method: ???
:return: the value of the attribute indicated by 'item'.
"""
- if item == "unit":
+ if item == 'datatype':
+ return self.datatype
+ if item == 'value':
+ try:
+ value = self.get_value()
+ if self._datatype == 'time':
+ value = serialize_time(value)
+ elif self._datatype == 'enum':
+ value = str(value)
+ except Exception as e:
+ raise DeviceException(f'Could not provide value of Measurement/Parameter {self.uuid}: {e}',
+ predecessor=e)
+
+ Variable.check_all(self._datatype, self._dimension, self._range, value)
+ return value
+ if item == 'dimension':
+ return self.dimension
+ if item == 'range':
+ return self.range
+ if item == 'unit':
return self._unit
- if item == 'nonce':
- return self._nonce
- if item == 'covariance':
- return self._covariance
- if item == 'uncertainty':
- return self._uncertainty
- if item == 'timestamp':
- return self._timestamp
if item == []:
return self
- return super().__getitem__(item, method)
+ return super().__getitem__(item)
- def __setitem__(self, key: str, value):
+ def __setitem__(self, key: str, value: Any):
"""
Setter - Method
If key is "value" datatype, dimension and range is checked for correctness.
:param key: sets the value of the attribute with name 'item' to the provided value.
:param value: value to be set
"""
- if key in ['value', 'timestamp', 'covariance', 'uncertainty']:
- raise KeyError('The {} attribute of a measurement can not be set manually!'.format(key))
- elif key == "nonce":
- self._nonce = self._nonce
- elif key == 'unit':
- self._unit = self._unit
- else:
- super().__setitem__(key, value)
+ if key in ['datatype', 'range', 'unit', 'dimension', 'value']:
+ raise Exception(f'{self.uuid}: The {key} can not be changed externally during runtime.')
+ super().__setitem__(key, value)
- def serialize(self, keys: [str], method=HTTP_GET):
+ def serialize(self, keys: List[str]) -> Dict[str, Any]:
"""
- Serializes an object of type Variable into a JSON-like dictionary.
+ Serializes an object of type Figure into a JSON-like dictionary.
:param keys: All attributes given in the "keys" array are serialized.
:param method: ???
:return: a dictionary having all "keys" as keys and the values of the corresponding attributes as value.
"""
# list is empty provide all attributes of the default-serialization
if not keys:
- keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range', 'timestamp', 'nonce', 'covariance', 'uncertainty',
- 'unit', 'ontology']
- # TODO temporary workaround, forces the server to always return the timestamp if the value is queried
- if 'value' in keys and 'timestamp' not in keys:
- keys += ['timestamp']
- dictionary = {}
+ keys = ['uuid', 'name', 'description', 'datatype', 'value', 'dimension', 'range', 'ontology']
# get all attribute values
+ dictionary = {}
for key in keys:
- value = self.__getitem__(key, method)
- # in case of timestamp convert into RFC3339 string
- if key == 'timestamp' or (key == 'value' and self._datatype == 'time'):
- value = value.isoformat() + 'Z' if value is not None else ""
+ value = self.__getitem__(key)
dictionary[key] = value
return dictionary
@staticmethod
- def deserialize(dictionary: Dict, implementation=None):
+ def check_dimension(dimension: List[int], value: Any):
"""
- Takes a JSON-like dictionary, parses it, performs a complete correctness check and returns an object of type Figure with the
- values provided in the dictionary, if dictionary is a valid serialization of a Figure.
- :param dictionary: serialized variable
- :param implementation: implementation wrapper object,
- :return: an object of type Figure
+ Checks whether the given value is of given dimension
+ :param dimension: the dimension the value provided by "value" should have
+ :param value: value to be checked for the dimension
"""
- # check if all required attributes are present
- if 'uuid' not in dictionary:
- raise SerialisationException('The variable can not be deserialized. UUID is missing!')
- uuid = dictionary['uuid']
- if 'name' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Name is missing!'.format(uuid))
- if 'description' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Description is missing!'.format(uuid))
- if 'datatype' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Datatype is missing!'.format(uuid))
- if 'dimension' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Dimension is missing!'.format(uuid))
- if 'value' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Value is missing!'.format(uuid))
- if 'range' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Range is missing!'.format(uuid))
- if 'unit' not in dictionary:
- raise SerialisationException('{}: The variable can not be deserialized. Unit is missing!'.format(uuid))
+ # dimension of undefined value must not be checked => valid
+ if value is None:
+ return
+
+ if not dimension:
+ if Variable.is_scalar(value):
+ # base case 1: dimension is empty and variable is a scalar => valid
+ return
+ else:
+ # base case 2: dimension is empty and variable is not a scalar => not valid
+ raise DimensionException('Figure of dimension 0 can not be of type list!')
+
try:
- ontology = dictionary['ontology'] if 'ontology' in dictionary else None
- return Variable(dictionary['uuid'], dictionary['name'], dictionary['description'], dictionary['datatype'], dictionary['dimension'],
- dictionary['range'], implementation, dictionary['unit'], ontology)
- except Exception as e:
- raise SerialisationException('{}: The variable can not be deserialized. {}'.format(uuid, e))
+ # base case 3: current dimension is fixed size "x" and length of the value is not "x" => not valid
+ if dimension[0] != 0 and len(value) != dimension[0]:
+ raise DimensionException('Dimension of data does not match dimension of variable!')
+ except TypeError as te:
+ raise DimensionException(str(te))
+ # recursion case
+ # at this point value is guaranteed to be of type list
+ # => recursively check the dimension of each "subvalue"
+ for v in value:
+ try:
+ Variable.check_dimension(dimension[1:], v)
+ except DimensionException as e:
+ raise e
+
+ @staticmethod
+ def check_type(datatype: Datatype, value: Any):
+ """
+ Checks if the given value is of the correct datatype. If value is not a scale, it checks all "subvalues" for correct datatype.
+ :param datatype: datatype the value provided by "value" should have
+ :param value: value to be checked for correct datatype
+ """
+ # datatype of undefined value must not be checked => valid
+ if value is None:
+ return
+ # base case: value is a scalar
+ if Variable.is_scalar(value):
+ # check if the type of value corresponds to given datatype
+ if datatype == Datatype.BOOL and not isinstance(value, bool):
+ raise TypeException(f'Boolean field does not match non-boolean value {value}!')
+ elif datatype == Datatype.INT and not isinstance(value, int):
+ raise TypeException(f'Integer field does not match non-integer value {value}!')
+ elif datatype == Datatype.FLOAT and not isinstance(value, float) and not isinstance(value, int):
+ raise TypeException(f'Floating point field does not match non-double value {value}!')
+ elif datatype == Datatype.STRING and not isinstance(value, str):
+ raise TypeException(f'String field does not match non-string value {value}!')
+ elif datatype == Datatype.ENUM and not isinstance(value, str): # TODO fix type check
+ raise TypeException(f'Enum field {value} must be a string!')
+ elif datatype == Datatype.TIME and not isinstance(value, str): # TODO fix type check
+ raise TypeException(f'Time field {value} must be string.')
+ elif datatype == Datatype.TIME and isinstance(value, str): # TODO fix type check
+ if value != '' and value is not None and not rfc3339.validate_rfc3339(value):
+ raise TypeException(f'Value is not a valid RFC3339-formatted timestring: {value}')
+ else:
+ raise TypeException(f'Unknown type descriptor: {datatype}')
+ else:
+ # recursion case: value is an array or matrix => check datatype of each 'subvalue' recursively
+ for v in value:
+ try:
+ Variable.check_type(datatype, v)
+ except TypeException as e:
+ raise e
+
+ @staticmethod
+ def check_range(datatype: Datatype, range: Range, value: Any):
+ """
+ Checks if the given value is within provided range (depending on the given datatype)
+
+ IMPORTANT: It is not checked whether the value is of correct type. If the type of value is not correct, the result
+ of check_range is not meaningful! To get expressive result check datatype before calling check_range!
+
+ :param datatype: datatype of the value
+ :param range: the range the value should be within
+ :param value: value to be checked for range
+
+ For all datatypes (except "bool" and "enum")the range specification is of the following form: [lower bound (LB), upper bound (UB)]
+ If LB or UB are None the value is unrestricted to the bottom or top, respectively.
+ In case of "int", "double" and "time" the interpretation of LB and UB is straightforward.
+ For "string" LB and UB restrict the length of the string. (If LB is given as None, 0 is the natural LB of cause)
+ "bool" is naturally bounded to "True" and "False", thus the range is not checked.
+ In case of "enum" the range contains the list of all possible values.
+ """
+ # if the list is empty, all values are possible
+ if not range:
+ if datatype == Datatype.ENUM: # TODO fix range check of enums
+ raise RangeException('A value of type enum must provide a range with possible values!')
+ else:
+ return
+ # base case: value is scalar => check if the value is in range
+ if Variable.is_scalar(value):
+ # bool is not checked, since there is only true and false
+ if datatype == Datatype.BOOL:
+ return
+ elif datatype == Datatype.INT and value is not None:
+ if range[0] is not None and value < range[0]:
+ raise RangeException(f'Integer value {value} is smaller than lower bound {range[0]}!')
+ elif range[1] is not None and value > range[1]:
+ raise RangeException(f'Integer value {value} is higher than upper bound {range[1]}!')
+ elif datatype == Datatype.FLOAT and value is not None:
+ if range[0] is not None and value < range[0]:
+ raise RangeException(f'Double value {value} is smaller than lower bound {range[0]}!')
+ elif range[1] is not None and value > range[1]:
+ raise RangeException(f'Double value {value} is higher than upper bound {range[1]}!')
+ elif datatype == Datatype.STRING and value is not None:
+ if range[0] is not None and len(value) < range[0]:
+ raise RangeException(f'String value {value} is too short. Minimal required length is {range[0]}!')
+ elif range[1] is not None and len(value) > range[1]:
+ raise RangeException(f'String value {value} is too long. Maximal allowed length is {range[1]}!')
+ elif datatype == Datatype.ENUM and value is not None: # TODO fix range check of enums
+ if value not in range:
+ raise RangeException(f'Enum value {value} is not within the set of allowed values!')
+ elif datatype == Datatype.TIME and value is not None and value != '':
+ if range[0] is not None:
+ if not rfc3339.validate_rfc3339(range[0]):
+ raise TypeException(
+ f'Can not check range of time value. Lower bound {range[0]} is not a valid RFC3339 timestring.')
+ if parse_time(value) < parse_time(range[0]):
+ raise RangeException(
+ f'Time value {parse_time(value)} is smaller than lower bound {parse_time(range[0])}!')
+ elif range[1] is not None:
+ if not rfc3339.validate_rfc3339(range[1]):
+ raise TypeException(
+ 'Can not check range of time value. Upper bound {} is not a valid RFC3339 timestring.')
+ if parse_time(value) > parse_time(range[1]):
+ raise RangeException(
+ f'Time value {parse_time(value)} is greater than upper bound {parse_time(range[1])}!')
+ else:
+ # recursion case: value is an array or matrix => check range of each "subvalue" recursively
+ for v in value:
+ try:
+ Variable.check_range(datatype, range, v)
+ except RangeException as e:
+ raise e
+
+ @staticmethod
+ def is_scalar(value: Any) -> bool:
+ return not isinstance(value, list)
+
+ @staticmethod
+ def check_all(datatype: Datatype, dimension: List[int], range: Range, value: Any):
+ Variable.check_type(datatype, value)
+ Variable.check_dimension(dimension, value)
+ Variable.check_range(datatype, range, value)
diff --git a/src/utils/const.py b/src/utils/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..553b2d21bb56500526c064f2af5fe8de8ae6445d
--- /dev/null
+++ b/src/utils/const.py
@@ -0,0 +1,8 @@
+import enum
+from typing import Union, Tuple
+
+HTTP_GET = 0
+HTTP_OPTIONS = 1
+BASE_UUID_PATTERN = r'[0-9A-Za-z-_]{3,}'
+
+Range = Union[Tuple[int, int], Tuple[str, str], enum.Enum]
\ No newline at end of file
diff --git a/src/utils/constants.py b/src/utils/constants.py
deleted file mode 100644
index 8ac340e21bace0baf074d4049a6729365a00868b..0000000000000000000000000000000000000000
--- a/src/utils/constants.py
+++ /dev/null
@@ -1,3 +0,0 @@
-HTTP_GET = 0
-HTTP_OPTIONS = 1
-BASE_UUID_PATTERN = r'[0-9A-Za-z-_]{3,}'
\ No newline at end of file
diff --git a/test/devices/experimental_lasertracker/Lasertracker.json b/test/devices/experimental_lasertracker/Lasertracker.json
new file mode 100644
index 0000000000000000000000000000000000000000..c32e28f2b938baddc24f6eb359210e650523ba43
--- /dev/null
+++ b/test/devices/experimental_lasertracker/Lasertracker.json
@@ -0,0 +1,355 @@
+{
+ "components": [
+ {
+ "components": [
+ {
+ "components": [],
+ "functions": [
+ {
+ "arguments": [
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [],
+ "value": 0,
+ "unit": "C81",
+ "uuid": "ARG-Azimuth",
+ "name": "Azimuth",
+ "description": "Azimuth angle the laser tracker head is jogged."
+ },
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [],
+ "value": 0,
+ "unit": "C81",
+ "uuid": "ARG-Elevation",
+ "name": "Elevation",
+ "description": "Elevation angle the laser tracker head is jogged."
+ }
+ ],
+ "returns": [],
+ "uuid": "FUN-Jog",
+ "name": "Jog",
+ "description": "Jogs the tracker head by the given angles for the azimuth and elevation."
+ },
+ {
+ "arguments": [
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [
+ 3
+ ],
+ "value": [
+ 0,
+ 0,
+ 0
+ ],
+ "unit": "MTR",
+ "uuid": "ARG-Position",
+ "name": "Position",
+ "description": "Position to which the laser tracker should point."
+ }
+ ],
+ "returns": [],
+ "uuid": "FUN-PointTo",
+ "name": "PointTo",
+ "description": "Moves the tracker head so that the laser points to the specified position."
+ }
+ ],
+ "measurements": [
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [
+ 3
+ ],
+ "value": [
+ 0,
+ 0,
+ 0
+ ],
+ "unit": "MTR",
+ "uuid": "MEA-Position",
+ "name": "Position",
+ "description": "Most recently dispatched measured position."
+ },
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [
+ 4
+ ],
+ "value": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "unit": "NONE",
+ "uuid": "MEA-Quaternion",
+ "name": "Quaternion",
+ "description": "Most recently dispatched measured orientation as quaternion, if available."
+ },
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [],
+ "value": 0,
+ "unit": "C81",
+ "uuid": "MEA-Azimuth",
+ "name": "Azimuth",
+ "description": "Current position of azimuth rotation encoder in Radian."
+ },
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [],
+ "value": 0,
+ "unit": "C81",
+ "uuid": "MEA-Elevation",
+ "name": "Elevation",
+ "description": "Current position of elevation rotation encoder."
+ },
+ {
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "double",
+ "dimension": [],
+ "value": 0,
+ "unit": "MTR",
+ "uuid": "MEA-Distance",
+ "name": "Distance",
+ "description": "Measured distance to the currently activate target."
+ },
+ {
+ "range": [
+ "False",
+ "True"
+ ],
+ "datatype": "bool",
+ "dimension": [
+ 2,
+ 3
+ ],
+ "value": [
+ [
+ true,
+ true,
+ true
+ ],
+ [
+ true,
+ true,
+ true
+ ]
+ ],
+ "unit": "NONE",
+ "uuid": "MEA-Online",
+ "name": "Online",
+ "description": "Some description..."
+ }
+ ],
+ "parameters": [
+ {
+ "constant": true,
+ "range": [
+ "OK",
+ "WARNING",
+ "ERROR",
+ "MAINTENANCE"
+ ],
+ "datatype": "enum",
+ "dimension": [],
+ "value": "ERROR",
+ "unit": "NONE",
+ "uuid": "PAR-State",
+ "name": "State",
+ "description": "Reflects the current state of the target. If logged in: OK. If not stable: WARNING. If lost: ERROR."
+ },
+ {
+ "constant": true,
+ "range": [
+ 0,
+ null
+ ],
+ "datatype": "string",
+ "dimension": [],
+ "value": "",
+ "unit": "NONE",
+ "uuid": "PAR-Calibration",
+ "name": "Calibration",
+ "description": "Unique identifier that can be used to retrieve a corresponding calibration certificate."
+ },
+ {
+ "constant": false,
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "int",
+ "dimension": [],
+ "value": 0,
+ "unit": "NONE",
+ "uuid": "PAR-Interval",
+ "name": "Interval",
+ "description": "Some description..."
+ }
+ ],
+ "uuid": "COM-Base",
+ "name": "Base",
+ "description": "Represents a base station in a distributed system. "
+ }
+ ],
+ "functions": [],
+ "measurements": [],
+ "parameters": [],
+ "uuid": "COM-BaseStations",
+ "name": "Base Stations",
+ "description": "Object acting as a list of base stations of the metrology system. "
+ },
+ {
+ "components": [
+ {
+ "uuid": "COM-Home-Target",
+ "name": "Home Target",
+ "description": "Represents an individual mobile entity ",
+ "parameters": [
+ {
+ "uuid": "PAR-State",
+ "value": "ERROR"
+ },
+ {
+ "uuid": "PAR-Mode",
+ "value": "Continuous"
+ },
+ {
+ "uuid": "PAR-Type",
+ "value": "SMR"
+ },
+ {
+ "uuid": "PAR-Calibration",
+ "value": ""
+ }
+ ],
+ "file": "./MobileEntities/Target.json"
+ }
+ ],
+ "functions": [],
+ "measurements": [],
+ "parameters": [],
+ "uuid": "COM-MobileEntities",
+ "name": "Mobile Entities",
+ "description": "Object acting as a list of mobile entities in the metrology system."
+ }
+ ],
+ "functions": [
+ {
+ "arguments": [],
+ "returns": [],
+ "uuid": "FUN-Reset",
+ "name": "Reset",
+ "description": "Resets the device into the state like directly after start-up."
+ },
+ {
+ "arguments": [],
+ "returns": [],
+ "uuid": "FUN-Shutdown",
+ "name": "Shutdown",
+ "description": "Gracefully shutdown the device."
+ }
+ ],
+ "measurements": [],
+ "parameters": [
+ {
+ "constant": false,
+ "range": [
+ "OK",
+ "WARNING",
+ "ERROR",
+ "MAINTENANCE"
+ ],
+ "datatype": "enum",
+ "dimension": [],
+ "value": "OK",
+ "unit": "NONE",
+ "uuid": "PAR-State",
+ "name": "State",
+ "description": "The current state of the device."
+ },
+ {
+ "constant": true,
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "string",
+ "dimension": [],
+ "value": "Laboratory for Machine Tools and Production Engineering WZL of RWTH Aachen",
+ "unit": "NONE",
+ "uuid": "PAR-Manufacturer",
+ "name": "Manufacturer",
+ "description": "Name of manufacturing company."
+ },
+ {
+ "constant": true,
+ "range": [
+ 0,
+ null
+ ],
+ "datatype": "int",
+ "dimension": [],
+ "value": 1,
+ "unit": "NONE",
+ "uuid": "PAR-Version",
+ "name": "Version",
+ "description": "Incremental API-Version."
+ },
+ {
+ "constant": true,
+ "range": [
+ null,
+ null
+ ],
+ "datatype": "time",
+ "dimension": [
+ 2
+ ],
+ "value": [
+ "2021-05-03T15:30:00.0000Z",
+ "2021-05-03T15:30:00.0000Z"
+ ],
+ "unit": "NONE",
+ "uuid": "PAR-Time",
+ "name": "Time",
+ "description": "Current system time."
+ }
+ ],
+ "uuid": "COM-Lasertracker",
+ "name": "Lasertracker",
+ "description": "Active coordinate measurement device based on laser interferometry for Large-Scale metrology applications."
+}
\ No newline at end of file
diff --git a/test/devices/experimental_lasertracker/MobileEntities/Target.json b/test/devices/experimental_lasertracker/MobileEntities/Target.json
new file mode 100644
index 0000000000000000000000000000000000000000..105272190780e915b002b9c21c938fa89f3cebec
--- /dev/null
+++ b/test/devices/experimental_lasertracker/MobileEntities/Target.json
@@ -0,0 +1 @@
+{"components": [], "functions": [{"arguments": [], "returns": [], "uuid": "FUN-Reset", "name": "Reset", "description": "Starts the search routine around the current direction."}, {"arguments": [], "returns": [], "uuid": "FUN-Trigger", "name": "Trigger", "description": "Trigger count measurements and set the resulting label to nonce. This function is only allowed in triggered acquisition mode."}], "measurements": [{"range": [null, null], "datatype": "double", "dimension": [3], "value": [0, 0, 0], "unit": "MTR", "uuid": "MEA-Position", "name": "Position", "description": "Most recently dispatched measured position."}, {"range": [null, null], "datatype": "double", "dimension": [4], "value": [0, 0, 0, 0], "unit": "NONE", "uuid": "MEA-Quaternion", "name": "Quaternion", "description": "Most recently dispatched measured orientation as quaternion, if available."}], "parameters": [{"constant": true, "range": ["OK", "WARNING", "ERROR", "MAINTENANCE"], "datatype": "enum", "dimension": [], "value": "ERROR", "unit": "NONE", "uuid": "PAR-State", "name": "State", "description": "Reflects the current state of the target. If logged in: OK. If not stable: WARNING. If lost: ERROR."}, {"constant": true, "range": ["Continuous", "Triggered", "External", "Idle"], "datatype": "enum", "dimension": [], "value": "Continuous", "unit": "NONE", "uuid": "PAR-Mode", "name": "Mode", "description": "Current state of the entity. In CONTINUOUS mode, values are dispatched as fast as possible. In TRIGGERED mode, values are only dispatches after a software trigger. In EXTERNAL mode, values are dispatched in accordance to an external trigger, e.g. probe or TTL. IDLE means the entity is currently not used."}, {"constant": true, "range": [null, null], "datatype": "string", "dimension": [], "value": "SMR", "unit": "NONE", "uuid": "PAR-Type", "name": "Type", "description": "System specific identifier of the target Type, e.g. SMR or Active SMR."}, {"constant": true, "range": [0, null], "datatype": "string", "dimension": [], "value": "", "unit": "NONE", "uuid": "PAR-Calibration", "name": "Calibration", "description": "Unique identifier that can be used to retrieve a corresponding calibration certificate."}, {"constant": false, "range": ["False", "True"], "datatype": "bool", "dimension": [], "value": false, "unit": "NONE", "uuid": "PAR-Locked", "name": "Locked", "description": "Some description..."}], "uuid": "COM-Target", "name": "Target", "description": "Represents an individual mobile entity "}
\ No newline at end of file
diff --git a/test/devices/experimental_lasertracker/__init__.py b/test/devices/experimental_lasertracker/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/devices/experimental_lasertracker/const.py b/test/devices/experimental_lasertracker/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1cd96ac3c340e9a462d96d05892966c632a3e9f
--- /dev/null
+++ b/test/devices/experimental_lasertracker/const.py
@@ -0,0 +1,10 @@
+MQTT_USERNAME = ''
+MQTT_PASSWORD = ''
+
+# MQTT_BROKER = 'wzl-mbroker01.wzl.rwth-aachen.de'
+# MQTT_PORT = 1883
+# MQTT_VHOST = "metrology"
+
+MQTT_BROKER = 'localhost'
+MQTT_PORT = 1883
+MQTT_VHOST = ""
diff --git a/test/devices/experimental_lasertracker/device/__init__.py b/test/devices/experimental_lasertracker/device/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/devices/experimental_lasertracker/device/com_base.py b/test/devices/experimental_lasertracker/device/com_base.py
new file mode 100644
index 0000000000000000000000000000000000000000..e75bccf59cab7077cf98a9ba0d030ad140995679
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/com_base.py
@@ -0,0 +1,38 @@
+from typing import List
+
+from device.enums import StateEnum
+from device.mea_azimuth import MEAAzimuth
+from device.mea_distance import MEADistance
+from device.mea_elevation import MEAElevation
+from device.mea_orientation import MEAOrientation
+from device.mea_position import MEAPosition
+from device.par_calibration import PARCalibration
+from device.par_interval import PARInterval
+from device.par_state import PARState
+from soil.component import Component
+from soil.device import Device
+
+
+class COMBase(Component):
+
+ def __init__(self, device: Device, topic: str):
+ Component.__init__(self, 'COM-Base', 'Base', 'Represents a base station in a distributed system.', device=device)
+ self._measurements += [MEAPosition(device, topic)]
+ self._measurements += [MEAOrientation(device, topic)]
+ self._measurements += [MEAAzimuth(device, topic)]
+ self._measurements += [MEAElevation(device, topic)]
+ self._measurements += [MEADistance(device, topic)]
+ self._parameters += [PARState(device, topic, StateEnum.OK)]
+ self._parameters += [PARCalibration(device, topic)]
+ self._parameters += [PARInterval(device, topic)]
+ self._functions += [FUNJog(device, topic)]
+ # TODO implement
+
+ def fun_jog(self, arg_azimuth: float = 0, arg_elevation: float = 0):
+ # TODO implement
+ pass
+
+ def fun_pointto(self, arg_position: List[float] = [0, 0, 0]):
+ arg_position = [0, 0, 0] if arg_position is None else arg_position
+ # TODO implement
+ pass
diff --git a/test/devices/experimental_lasertracker/device/com_basestations.py b/test/devices/experimental_lasertracker/device/com_basestations.py
new file mode 100644
index 0000000000000000000000000000000000000000..c270557adb6ac8160149cf075a637da615f0c00e
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/com_basestations.py
@@ -0,0 +1,11 @@
+from device.com_base import COMBase
+from soil.component import Component
+from soil.device import Device
+
+
+class COMBaseStations(Component):
+
+ def __init__(self, device: Device, topic: str):
+ Component.__init__(self, 'COM-BaseStations', 'Base Stations',
+ 'Object acting as a list of mobile entities in the metrology system.', device=device)
+ self._components += [COMBase(device, f'{topic}/COM-Base')]
diff --git a/test/devices/experimental_lasertracker/device/com_lasertracker.py b/test/devices/experimental_lasertracker/device/com_lasertracker.py
new file mode 100644
index 0000000000000000000000000000000000000000..56dca361e286e0f2baf3f67db9392a45477acb09
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/com_lasertracker.py
@@ -0,0 +1,60 @@
+import datetime
+from typing import List
+
+from device.com_basestations import COMBaseStations
+from device.com_mobileentities import COMMobileEntities
+from device.enums import StateEnum
+from soil.component import Component
+
+from soil.stream import Job
+
+
+class COMLasertracker(Component):
+
+ def __init__(self, device):
+ self._device = device
+ self._par_state = StateEnum.OK
+ self._par_manufacturer = "Laboratory for Machine Tools and Production Engineering WZL of RWTH Aachen"
+ self._par_version = 1
+ self._par_time = [datetime.datetime.fromtimestamp(1620048600.0), datetime.datetime.fromtimestamp(1620048600.0)]
+ self._com_basestations = COMBaseStations(device)
+ self._com_mobileentities = COMMobileEntities(device)
+ # TODO implement
+
+ def get_jobs(self, topic: str) -> List[Job]:
+ job_list = []
+ job_list += self._com_basestations.get_jobs(f"{topic}/COM-BaseStations")
+ job_list += self._com_mobileentities.get_jobs(f"{topic}/COM-MobileEntities")
+ return job_list
+
+
+ def get_par_state(self) -> StateEnum:
+ # TODO implement
+ return self._par_state
+
+ def set_par_state(self, par_state: StateEnum):
+ # TODO implement
+ self._par_state = par_state
+
+ def get_par_manufacturer(self) -> str:
+ # TODO implement
+ return self._par_manufacturer
+
+ def get_par_version(self) -> int:
+ # TODO implement
+ return self._par_version
+
+ def get_par_time(self) -> List['datetime']:
+ # TODO implement
+ return self._par_time
+
+
+ def fun_reset(self):
+ # TODO implement
+ pass
+
+
+ def fun_shutdown(self):
+ # TODO implement
+ pass
+
\ No newline at end of file
diff --git a/test/devices/experimental_lasertracker/device/com_mobileentities.py b/test/devices/experimental_lasertracker/device/com_mobileentities.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ed738fc674631147862a2d026f6c13d1ed2a928
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/com_mobileentities.py
@@ -0,0 +1,27 @@
+from typing import Dict, List
+
+from device.com_target import COMTarget
+from device.enums import StateEnum, ModeEnum
+from soil.stream import Job
+from device.component import Component
+
+
+class COMMobileEntities(Component):
+
+ def __init__(self, device):
+ self._device = device
+ self._com_target = {}
+ self._com_target["COM-Home-Target"] = COMTarget(device, StateEnum.ERROR, ModeEnum.Continuous, "SMR", "")
+ # TODO implement
+
+ def get_jobs(self, topic: str) -> List[Job]:
+ job_list = []
+ for uuid in self._com_target:
+ job_list += self._com_target[uuid].get_jobs(f"{topic}/{uuid}")
+ return job_list
+
+ def add(self, uuid: str, child: COMTarget):
+ self._com_target[uuid] = child
+
+ def remove(self, uuid: str):
+ del self._com_target[uuid]
diff --git a/test/devices/experimental_lasertracker/device/com_target.py b/test/devices/experimental_lasertracker/device/com_target.py
new file mode 100644
index 0000000000000000000000000000000000000000..6198c4ad33a8ae18b7693fb9defd7d496a63a14b
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/com_target.py
@@ -0,0 +1,72 @@
+from typing import Dict
+
+from typing import List
+from device.enums import StateEnum, ModeEnum
+
+from soil.event import Event, EventSeverity, EventTrigger
+from soil.stream import Job, FixedJob, UpdateJob, EventJob
+from device.component import Component
+
+
+class COMTarget(Component):
+
+ def __init__(self, device, par_state: StateEnum = StateEnum.ERROR, par_mode: ModeEnum = ModeEnum.Continuous,
+ par_type: str = "SMR", par_calibration: str = "", par_locked: bool = False):
+ self._device = device
+ self._mea_position = [0, 0, 0]
+ self._mea_quaternion = [0, 0, 0, 0]
+ self._par_state = par_state
+ self._par_mode = par_mode
+ self._par_type = par_type
+ self._par_calibration = par_calibration
+ self._par_locked = par_locked
+ # TODO implement
+
+ def get_jobs(self, topic: str) -> List[Job]:
+ job_list = []
+ job_list += [FixedJob(f"{topic}/MEA-Position", 1, self.get_mea_position)]
+ job_list += [UpdateJob(f"{topic}/MEA-Quaternion", self.get_mea_quaternion)]
+ job_list += [EventJob(f"{topic}/PAR-Locked", 10, self.get_par_locked,
+ Event(EventSeverity.WARNING, EventTrigger.EQUALS, "bool", False,
+ "Target is not locked!"))]
+ return job_list
+
+ def get_mea_position(self) -> List[float]:
+ # TODO implement
+ return self._mea_position
+
+ def get_mea_quaternion(self) -> List[float]:
+ # TODO implement
+ return self._mea_quaternion
+
+ def get_par_state(self) -> StateEnum:
+ # TODO implement
+ return self._par_state
+
+ def get_par_mode(self) -> ModeEnum:
+ # TODO implement
+ return self._par_mode
+
+ def get_par_type(self) -> str:
+ # TODO implement
+ return self._par_type
+
+ def get_par_calibration(self) -> str:
+ # TODO implement
+ return self._par_calibration
+
+ def get_par_locked(self) -> bool:
+ # TODO implement
+ return self._par_locked
+
+ def set_par_locked(self, par_locked: bool):
+ # TODO implement
+ self._par_locked = par_locked
+
+ def fun_reset(self):
+ # TODO implement
+ pass
+
+ def fun_trigger(self):
+ # TODO implement
+ pass
diff --git a/test/devices/experimental_lasertracker/device/enums.py b/test/devices/experimental_lasertracker/device/enums.py
new file mode 100644
index 0000000000000000000000000000000000000000..deaf5f3c0885da1197cd00864c773bcca0b0347e
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/enums.py
@@ -0,0 +1,31 @@
+from enum import Enum
+
+
+class StateEnum(Enum):
+ OK = 0
+ WARNING = 1
+ ERROR = 2
+ MAINTENANCE = 3
+
+ def __str__(self):
+ return ["OK", "WARNING", "ERROR", "MAINTENANCE"][self.value]
+
+ @classmethod
+ def from_string(cls, name):
+ return {"OK": StateEnum.OK, "WARNING": StateEnum.WARNING, "ERROR": StateEnum.ERROR,
+ "MAINTENANCE": StateEnum.MAINTENANCE}[name]
+
+
+class ModeEnum(Enum):
+ Continuous = 0
+ Triggered = 1
+ External = 2
+ Idle = 3
+
+ def __str__(self):
+ return ["Continuous", "Triggered", "External", "Idle"][self.value]
+
+ @classmethod
+ def from_string(cls, name):
+ return {"Continuous": ModeEnum.Continuous, "Triggered": ModeEnum.Triggered, "External": ModeEnum.External,
+ "Idle": ModeEnum.Idle}[name]
diff --git a/test/devices/experimental_lasertracker/device/fun_jog.py b/test/devices/experimental_lasertracker/device/fun_jog.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f2ceafdba520e2d42d1bf9d5c2464f229eb5434
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/fun_jog.py
@@ -0,0 +1,12 @@
+from soil.device import Device
+from soil.function import Function
+
+
+class FUNJog(Function):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Function.__init__(self, 'FUN-Jog', 'jog',
+ 'Jogs the tracker head by the given angles for the azimuth and elevation.', device=device)
+
+ def execute(self, *args, **kwargs):
+ pass
diff --git a/test/devices/experimental_lasertracker/device/mea_azimuth.py b/test/devices/experimental_lasertracker/device/mea_azimuth.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f5e04adaf1259d78736d8a303840f73d1204af9
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/mea_azimuth.py
@@ -0,0 +1,11 @@
+from soil.device import Device
+
+from soil.datatype import Datatype
+from soil.measurement import Measurement
+
+
+class MEAAzimuth(Measurement):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Measurement.__init__(self, "MEA-Azimuth", "Azimuth", "Current position of azimuth rotation encoder in Radian.",
+ Datatype.FLOAT, [], (0, 3.14), "C81", device=device)
diff --git a/test/devices/experimental_lasertracker/device/mea_distance.py b/test/devices/experimental_lasertracker/device/mea_distance.py
new file mode 100644
index 0000000000000000000000000000000000000000..b194eba82c05e5bdf2acad33972bf0aec0fcbb0b
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/mea_distance.py
@@ -0,0 +1,16 @@
+from soil.device import Device
+
+from soil.datatype import Datatype
+from soil.event import Event, EventSeverity, EventTrigger
+from soil.measurement import Measurement
+from soil.stream import EventJob
+
+
+class MEADistance(Measurement):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Measurement.__init__(self, "MEA-Distance", "Distance", "Measured distance to the currently activate target.",
+ Datatype.FLOAT, [], (0, 80), "MTR", device=device)
+ self._jobs += [EventJob(f"{parent_topic}/MEA-Distance", 10, self.get_value,
+ Event(EventSeverity.WARNING, EventTrigger.LARGER, "double", 50,
+ "The target is distance is large. Uncertainty might be high."))]
diff --git a/test/devices/experimental_lasertracker/device/mea_elevation.py b/test/devices/experimental_lasertracker/device/mea_elevation.py
new file mode 100644
index 0000000000000000000000000000000000000000..152687d550d4d6d553341ca540e09686015d41b6
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/mea_elevation.py
@@ -0,0 +1,12 @@
+from soil.device import Device
+
+from soil.datatype import Datatype
+from soil.measurement import Measurement
+
+
+class MEAElevation(Measurement):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Measurement.__init__(self, "MEA-Elevation", "Elevation",
+ "Current position of elevation rotation encoder in Radian.", Datatype.FLOAT, [], (0, 3.14),
+ "C81", device=device)
diff --git a/test/devices/experimental_lasertracker/device/mea_orientation.py b/test/devices/experimental_lasertracker/device/mea_orientation.py
new file mode 100644
index 0000000000000000000000000000000000000000..186fc1c94a93bd7a410ca1088bda366889daa292
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/mea_orientation.py
@@ -0,0 +1,10 @@
+from soil.device import Device
+from soil.datatype import Datatype
+from soil.measurement import Measurement
+
+
+class MEAOrientation(Measurement):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Measurement.__init__(self, "MEA-Orientation", "Orientation", "Most recently dispatched measured orientation as quaternion, if available.",
+ Datatype.FLOAT, [3], (0, 1), "None", device=device)
diff --git a/test/devices/experimental_lasertracker/device/mea_position.py b/test/devices/experimental_lasertracker/device/mea_position.py
new file mode 100644
index 0000000000000000000000000000000000000000..d4648532325b3b6dfa9b176db0c887d1a95f2785
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/mea_position.py
@@ -0,0 +1,14 @@
+from soil.datatype import Datatype
+from soil.device import Device
+from soil.measurement import Measurement
+from soil.stream import FixedJob
+
+
+class MEAPosition(Measurement):
+
+ def __init__(self, device: Device, parent_topic: str):
+ Measurement.__init__(self, "MEA-Position", "Position", "Most recently dispatched measured position.",
+ Datatype.FLOAT, [3], (0, 80), "MTR", device=device)
+
+ # create jobs
+ self._jobs += [FixedJob(f"{parent_topic}/{self.uuid}", 1, self.get_value)]
diff --git a/test/devices/experimental_lasertracker/device/par_calibration.py b/test/devices/experimental_lasertracker/device/par_calibration.py
new file mode 100644
index 0000000000000000000000000000000000000000..f8dbaca0a2f13b248199ffdc26bad0e742421049
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/par_calibration.py
@@ -0,0 +1,11 @@
+from soil.datatype import Datatype
+from soil.device import Device
+from soil.parameter import Parameter
+
+
+class PARCalibration(Parameter):
+
+ def __init__(self, device: Device, parent_topic: str, value: str = ""):
+ Parameter.__init__(self, "PAR-Calibration", "Calibration",
+ "Unique identifier that can be used to retrieve a corresponding calibration certificate.",
+ Datatype.STRING, [], (0, None), constant=True, value=value, device=device)
diff --git a/test/devices/experimental_lasertracker/device/par_interval.py b/test/devices/experimental_lasertracker/device/par_interval.py
new file mode 100644
index 0000000000000000000000000000000000000000..c539a6deeba847eb9d27d128e9cfae04ebccb84e
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/par_interval.py
@@ -0,0 +1,13 @@
+from device.enums import StateEnum
+from soil.datatype import Datatype
+from soil.device import Device
+from soil.event import Event, EventSeverity, EventTrigger
+from soil.parameter import Parameter
+from soil.stream import EventJob
+
+
+class PARInterval(Parameter):
+
+ def __init__(self, device: Device, parent_topic: str, value: int = 10):
+ Parameter.__init__(self, "PAR-Interval", "Interval", "An interval.",
+ Datatype.ENUM, [], StateEnum, constant=False, value=value, device=device)
diff --git a/test/devices/experimental_lasertracker/device/par_state.py b/test/devices/experimental_lasertracker/device/par_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f0eb3963937590633bbc25987f5567dc35a4d84
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/par_state.py
@@ -0,0 +1,16 @@
+from device.enums import StateEnum
+from soil.datatype import Datatype
+from soil.device import Device
+from soil.event import Event, EventSeverity, EventTrigger
+from soil.parameter import Parameter
+from soil.stream import EventJob
+
+
+class PARState(Parameter):
+
+ def __init__(self, device: Device, parent_topic: str, value: StateEnum = StateEnum.ERROR):
+ Parameter.__init__(self, "PAR-State", "State", "Status of the base station.",
+ Datatype.ENUM, [], StateEnum, constant=False, value=value, device=device)
+ self._jobs += [EventJob(f"{parent_topic}/PAR-State", 10, self.get_value,
+ Event(EventSeverity.ERROR, EventTrigger.EQUALS, "enum", StateEnum.ERROR,
+ "An error occured!"))]
\ No newline at end of file
diff --git a/test/devices/experimental_lasertracker/device/start.py b/test/devices/experimental_lasertracker/device/start.py
new file mode 100644
index 0000000000000000000000000000000000000000..b7b26db55f241d933bac0c99e69dd49f2ae5354d
--- /dev/null
+++ b/test/devices/experimental_lasertracker/device/start.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import datetime
+import sys
+from concurrent.futures import ThreadPoolExecutor
+
+from devices.lasertracker.const import MQTT_USERNAME, MQTT_PASSWORD, MQTT_BROKER, MQTT_VHOST, MQTT_PORT
+from wzl.utilities import root_logger
+from wzl.mqtt import MQTTPublisher
+from src.soil.component import Component
+from src.http.server_old import HTTPServer
+from src.soil.event import Event, EventSeverity, EventTrigger
+from src.soil.stream import FixedJob, DynamicJob, EventJob, UpdateJob, StreamScheduler
+
+from devices.lasertracker.device.enums import StateEnum, ModeEnum
+from devices.lasertracker.device.com_lasertracker import COMLasertracker
+
+sys.setswitchinterval(0.0005)
+
+
+def start(com_lasertracker: COMLasertracker):
+ # server settings
+ address = '127.0.0.1'
+ port = '8000'
+
+ main_logger = root_logger.get(__name__)
+
+ # set up servers
+ executor = ThreadPoolExecutor(max_workers=100)
+ loop = asyncio.get_event_loop()
+ loop.set_default_executor(executor)
+
+ # configure mqtt
+ mqtt = MQTTPublisher(MQTT_USERNAME)
+ mqtt.connect(MQTT_BROKER, MQTT_PORT, MQTT_VHOST + ":" + MQTT_USERNAME, MQTT_PASSWORD)
+
+ # configure messages
+ scheduler = StreamScheduler(loop, com_lasertracker.get_jobs("COM-Lasertracker"), [mqtt])
+
+ # configure model
+ mapping = {}
+ mapping['COM-Lasertracker'] = {'add': None, 'remove': None}
+ submapping = mapping['COM-Lasertracker']
+ submapping['PAR-State'] = {'getter': com_lasertracker.get_par_state, 'setter': com_lasertracker.set_par_state}
+ submapping['PAR-Manufacturer'] = {'getter': com_lasertracker.get_par_manufacturer, 'setter': None}
+ submapping['PAR-Version'] = {'getter': com_lasertracker.get_par_version, 'setter': None}
+ submapping['PAR-Time'] = {'getter': com_lasertracker.get_par_time, 'setter': None}
+ submapping['FUN-Reset'] = {'method': com_lasertracker.fun_reset, 'signature': {'arguments': {}, 'returns': []}}
+ submapping['FUN-Shutdown'] = {'method': com_lasertracker.fun_shutdown, 'signature': {'arguments': {}, 'returns': []}}
+ mapping['COM-Lasertracker']['COM-BaseStations'] = {'add': None, 'remove': None}
+ submapping = mapping['COM-Lasertracker']['COM-BaseStations']
+ mapping['COM-Lasertracker']['COM-BaseStations']['COM-Base'] = {'add': None, 'remove': None}
+ submapping = mapping['COM-Lasertracker']['COM-BaseStations']['COM-Base']
+ submapping['MEA-Position'] = com_lasertracker._com_basestations._com_base.get_mea_position
+ submapping['MEA-Quaternion'] = com_lasertracker._com_basestations._com_base.get_mea_quaternion
+ submapping['MEA-Azimuth'] = com_lasertracker._com_basestations._com_base.get_mea_azimuth
+ submapping['MEA-Elevation'] = com_lasertracker._com_basestations._com_base.get_mea_elevation
+ submapping['MEA-Distance'] = com_lasertracker._com_basestations._com_base.get_mea_distance
+ submapping['MEA-Online'] = com_lasertracker._com_basestations._com_base.get_mea_online
+ submapping['PAR-State'] = {'getter': com_lasertracker._com_basestations._com_base.get_par_state, 'setter': None}
+ submapping['PAR-Calibration'] = {'getter': com_lasertracker._com_basestations._com_base.get_par_calibration, 'setter': None}
+ submapping['PAR-Interval'] = {'getter': com_lasertracker._com_basestations._com_base.get_par_interval,
+ 'setter': com_lasertracker._com_basestations._com_base.set_par_interval}
+ submapping['FUN-Jog'] = {'method': com_lasertracker._com_basestations._com_base.fun_jog,
+ 'signature': {'arguments': {'ARG-Azimuth': 'arg_azimuth', 'ARG-Elevation': 'arg_elevation'}, 'returns': []}}
+ submapping['FUN-PointTo'] = {'method': com_lasertracker._com_basestations._com_base.fun_pointto,
+ 'signature': {'arguments': {'ARG-Position': 'arg_position'}, 'returns': []}}
+ mapping['COM-Lasertracker']['COM-MobileEntities'] = {'add': com_lasertracker._com_mobileentities.add,
+ 'remove': com_lasertracker._com_mobileentities.remove}
+ submapping = mapping['COM-Lasertracker']['COM-MobileEntities']
+ for child0_uuid in com_lasertracker._com_mobileentities._com_target:
+ mapping['COM-Lasertracker']['COM-MobileEntities'][child0_uuid] = {'add': None, 'remove': None}
+ submapping = mapping['COM-Lasertracker']['COM-MobileEntities'][child0_uuid]
+ submapping['MEA-Position'] = com_lasertracker._com_mobileentities._com_target[child0_uuid].get_mea_position
+ submapping['MEA-Quaternion'] = com_lasertracker._com_mobileentities._com_target[child0_uuid].get_mea_quaternion
+ submapping['PAR-State'] = {'getter': com_lasertracker._com_mobileentities._com_target[child0_uuid].get_par_state, 'setter': None}
+ submapping['PAR-Mode'] = {'getter': com_lasertracker._com_mobileentities._com_target[child0_uuid].get_par_mode, 'setter': None}
+ submapping['PAR-Type'] = {'getter': com_lasertracker._com_mobileentities._com_target[child0_uuid].get_par_type, 'setter': None}
+ submapping['PAR-Calibration'] = {'getter': com_lasertracker._com_mobileentities._com_target[child0_uuid].get_par_calibration, 'setter': None}
+ submapping['PAR-Locked'] = {'getter': com_lasertracker._com_mobileentities._com_target[child0_uuid].get_par_locked,
+ 'setter': com_lasertracker._com_mobileentities._com_target[child0_uuid].set_par_locked}
+ submapping['FUN-Reset'] = {'method': com_lasertracker._com_mobileentities._com_target[child0_uuid].fun_reset,
+ 'signature': {'arguments': {}, 'returns': []}}
+ submapping['FUN-Trigger'] = {'method': com_lasertracker._com_mobileentities._com_target[child0_uuid].fun_trigger,
+ 'signature': {'arguments': {}, 'returns': []}}
+ model = Component.load('./Lasertracker.json', mapping['COM-Lasertracker'])
+
+ http = HTTPServer(loop, address, port, model)
+
+ # start servers
+ main_logger.info("Starting main asynchronous loop")
+ try:
+ loop.run_forever()
+ except:
+ pass
+ finally:
+ loop.close()
diff --git a/test/devices/experimental_lasertracker/lasertracker.py b/test/devices/experimental_lasertracker/lasertracker.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6bd99caafac17159422f6d96fb42d17ba191e7c
--- /dev/null
+++ b/test/devices/experimental_lasertracker/lasertracker.py
@@ -0,0 +1,30 @@
+import datetime
+import random
+
+from device.enums import StateEnum
+from device.com_lasertracker import COMLasertracker
+
+
+class Lasertracker(COMLasertracker):
+
+ def __init__(self, device):
+ COMLasertracker.__init__(self, device)
+
+ def get_par_state(self):
+ if random.random() < 0.5:
+ self._par_state = StateEnum.OK
+ else:
+ self._par_state = StateEnum.WARNING
+ return self._par_state
+
+ def get_par_time(self):
+ # self._par_time = datetime.datetime.now()
+ return self._par_time
+
+ def fun_reset(self):
+ # TODO implement
+ pass
+
+ def fun_shutdown(self):
+ # TODO implement
+ pass
diff --git a/test/devices/experimental_lasertracker/main.py b/test/devices/experimental_lasertracker/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a8d7c3ddd47d3f93b97babbc991720fb8c63a49
--- /dev/null
+++ b/test/devices/experimental_lasertracker/main.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from soil.device import Device
+from device.start import start
+from lasertracker import Lasertracker
+
+if __name__ == "__main__":
+
+ device = Device()
+ lasertracker = Lasertracker(device)
+
+ start(lasertracker)
\ No newline at end of file
diff --git a/test/devices/lasertracker/device/start.py b/test/devices/lasertracker/device/start.py
index f1d7630bf746d3709d7208470228fd7de4bd475c..dcde8780092353e53d9a313ddda5bf2fed46c35a 100644
--- a/test/devices/lasertracker/device/start.py
+++ b/test/devices/lasertracker/device/start.py
@@ -10,7 +10,7 @@ from wzl.mqtt import MQTTPublisher
from src.soil.component import Component
from src.http.server import HTTPServer
from src.soil.event import Event, EventSeverity, EventTrigger
-from src.soil.stream import FixedJob, ConfigurableJob, EventJob, UpdateJob, MessageScheduler, EventScheduler, UpdateScheduler
+from src.soil.stream import FixedJob, DynamicJob, EventJob, UpdateJob, MessageScheduler, EventScheduler, UpdateScheduler
from devices.lasertracker.device.enums import StateEnum, ModeEnum
from devices.lasertracker.device.com_lasertracker import COMLasertracker