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