307 lines
14 KiB
Python
307 lines
14 KiB
Python
from .datatypes import CompositeBoolean, BattOpMode, CompositeMeasurement
|
|
from .datatypes import FlowBatteryState, ConvState, ConvOpMode
|
|
from .datatypes import HeatCirculationPumpMode, HeatCirculationPumpState
|
|
import syslab.config as config
|
|
|
|
|
|
class SyslabUnit:
|
|
__HIDDEN_RESOURCES = {
|
|
'authenticate': ('boolean', 'String', 'String'),
|
|
'isAuthenticated': ('boolean',),
|
|
'checkConnection': ('boolean',),
|
|
'logout': ('void',)}
|
|
|
|
def __init__(self, baseurl, units=None, which=None, host=None, port=None, unit_name=None, unit_type=""):
|
|
"""
|
|
Initialize a proxy to the given unit.
|
|
|
|
:param baseurl: Example: 'http://{host}:{port}/typebased_WebService_Battery/VRBBatteryWebService/{unit_name}/'
|
|
:param units: dictionary of units to be loaded in the format {'which': ('host', 'port', 'unit_name')}
|
|
:param which: string indicating which unit we pick from _units_
|
|
:param host: (optional) override host
|
|
:param port: (optional) override port
|
|
:param unit_name: (optional) override unit_name
|
|
:param unit_type: (optional) Used to indicate the type of the unit.
|
|
"""
|
|
|
|
if units is None:
|
|
units = {}
|
|
|
|
if which is not None:
|
|
if which not in units:
|
|
raise TypeError(
|
|
'The {unit_type} should be one of: "{list_of_units}"'.format(
|
|
unit_type=unit_type,
|
|
list_of_units='", "'.join(units.keys())
|
|
))
|
|
else:
|
|
host, port, unit_name = units.get(which)
|
|
if host is not None:
|
|
host = host
|
|
if port is not None:
|
|
port = port
|
|
|
|
assert host is not None, "When assigning custom port, host must not be None."
|
|
assert port is not None, "When assigning custom host, port must not be None."
|
|
assert unit_name is not None, "When assigning custom host and port, unit_name must not be None."
|
|
|
|
url = baseurl.format(host=host, port=port, unit_name=unit_name)
|
|
print(url)
|
|
self._url = url
|
|
self._resources = parse_resources(url)
|
|
self._resources = {**self._resources, **SyslabUnit.__HIDDEN_RESOURCES}
|
|
# TODO: Do type checking on these types. Ignore for now
|
|
self._complex_arg_types = ['BattOpMode', 'CompositeMeasurement', 'HeatCirculationPumpMode']
|
|
|
|
|
|
if config.REMOTE_LOGGER:
|
|
#import logger
|
|
if config.DEBUG:
|
|
print(f"Setting up remote logger with default IP: {config.REMOTE_IP} and port: {config.REMOTE_PORT}")
|
|
from uuid import uuid1, getnode
|
|
from ..comm.LogUtils import setup_udp_logger
|
|
self.__logger = setup_udp_logger(config.SPLOG_NAME)
|
|
self.__session_id = uuid1().__str__()
|
|
self.__host_id = getnode().__str__()
|
|
|
|
# TODO: Set up local setpoints Logger
|
|
if config.LOCAL_LOGGER:
|
|
pass
|
|
|
|
def __request_int(self, resource, method, args, body) -> int:
|
|
return int(send_request(self._url, resource, method, args, body))
|
|
|
|
def __request_float(self, resource, method, args, body) -> float:
|
|
return float(send_request(self._url, resource, method, args, body))
|
|
|
|
def __request_boolean(self, resource, method, args, body) -> bool:
|
|
return bool(send_request(self._url, resource, method, args, body))
|
|
|
|
def __request_string(self, resource, method, args, body) -> str:
|
|
return send_request(self._url, resource, method, args, body)
|
|
|
|
def __request_void(self, resource, method, args, body) -> None:
|
|
send_request(self._url, resource, method, args, body)
|
|
|
|
def __request_composite_boolean(self, resource, method, args, body) -> CompositeBoolean:
|
|
|
|
if self.__get_resource_return_type(resource) == 'CompositeBoolean':
|
|
json_string = self.__request_string(resource, method, args, body)
|
|
result = CompositeBoolean.parseFromJSON(json_string)
|
|
else:
|
|
raise TypeError('Error: resource "{0}" does not return a CompositeBoolean.'.format(resource))
|
|
|
|
return result
|
|
|
|
def __request_composite_measurement(self, resource, method, args, body) -> CompositeMeasurement:
|
|
|
|
if self.__get_resource_return_type(resource) == 'CompositeMeasurement':
|
|
json_string = self.__request_string(resource, method, args, body)
|
|
result = CompositeMeasurement.parseFromJSON(json_string)
|
|
else:
|
|
raise TypeError('Error: resource "{0}" does not return a CompositeMeasurement.'.format(resource))
|
|
|
|
return result
|
|
|
|
def _request_resource(self, resource, args=(), method='get', body=None, check_types=True):
|
|
if not type(resource) is str:
|
|
raise TypeError("Resource should be a string, found {0}.".format(type(resource)))
|
|
|
|
if resource not in self._resources:
|
|
# print(resource)
|
|
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
|
|
if not resource[-1] in ('1', '2'):
|
|
raise ValueError('Unknown resource: {0} - no such resource found for {1}'.format(resource, self._url))
|
|
|
|
if type(args) is int or type(args) is float or type(args) is str:
|
|
args = (args,)
|
|
|
|
if config.DEBUG:
|
|
print("Resource: ", resource, " and args: ", args, "with method: ", method)
|
|
|
|
if config.REMOTE_LOGGER and method == 'put':
|
|
logstr = f'SessionID:{self.__session_id}||SenderID:{self.__host_id}||Unit-URL:{self._url}||Setterfcn:{resource}||ArgValue:{args}'
|
|
self.__logger.info(logstr)
|
|
|
|
return_type = self.__get_resource_return_type(resource)
|
|
self.__check_argument(resource, args)
|
|
result = None
|
|
|
|
if check_types:
|
|
try:
|
|
if return_type == 'CompositeMeasurement':
|
|
result = self.__request_composite_measurement(resource, method, args, body)
|
|
elif return_type == 'CompositeBoolean':
|
|
result = self.__request_composite_boolean(resource, method, args, body)
|
|
elif return_type == 'String':
|
|
result = self.__request_string(resource, method, args, body)
|
|
elif return_type == 'int':
|
|
result = self.__request_int(resource, method, args, body)
|
|
elif return_type == 'boolean':
|
|
result = self.__request_boolean(resource, method, args, body)
|
|
elif return_type == 'float' or return_type == 'double':
|
|
result = self.__request_float(resource, method, args, body)
|
|
elif return_type == 'void':
|
|
self.__request_string(resource, method, args, body)
|
|
elif return_type == 'BattOpMode':
|
|
result = BattOpMode.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'FlowBatteryState':
|
|
result = FlowBatteryState.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'ConvState':
|
|
result = ConvState.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'EVSEState':
|
|
result = EVSEState.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'ConvOpMode':
|
|
result = ConvOpMode.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'HeatCirculationPumpMode':
|
|
result = HeatCirculationPumpMode.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'HeatCirculationPumpState':
|
|
result = HeatCirculationPumpState.parseFromJSON(self.__request_string(resource, method, args, body))
|
|
elif return_type == 'WYEV' or return_type == 'WYEA' or return_type == 'DELV':
|
|
import json
|
|
result = json.loads(self.__request_string(resource, method, args, body))
|
|
else:
|
|
raise TypeError(
|
|
'TypeError: The return type {0} by {1} is unknown or not supported yet.'.format(return_type,
|
|
resource))
|
|
except KeyError as e:
|
|
# raise e
|
|
raise ValueError('{0} - no such resource found for {1}'.format(resource, self._url))
|
|
else:
|
|
import json
|
|
return_string = self.__request_string(resource, method, args, body)
|
|
try:
|
|
result = json.loads(return_string)
|
|
except RecursionError:
|
|
raise RecursionError(
|
|
"Maximum recursion depth exceeded while decoding "
|
|
"a JSON array from a unicode string. Length: {0}".format(
|
|
len(return_string)))
|
|
|
|
return result
|
|
|
|
def __get_resource_return_type(self, resource):
|
|
if not resource[-1] in ('1', '2'):
|
|
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
|
|
return self._resources[resource][0]
|
|
else:
|
|
return self._resources[resource[:-1]][0]
|
|
|
|
def __get_resource_argument_types(self, resource):
|
|
if not resource[-1] in ('1', '2'):
|
|
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
|
|
return self._resources[resource][1:]
|
|
else:
|
|
return self._resources[resource[:-1]][1:]
|
|
|
|
def __check_argument(self, resource, args):
|
|
arg_types = self.__get_resource_argument_types(resource)
|
|
try:
|
|
# TODO Ignore complex arguments (for now)
|
|
for complex_arg_type in self._complex_arg_types:
|
|
if complex_arg_type in arg_types:
|
|
return
|
|
|
|
if len(args) == len(arg_types):
|
|
for arg, arg_type in zip(args, arg_types):
|
|
|
|
if arg_type == 'String' and type(arg) is not str:
|
|
raise TypeError()
|
|
elif arg_type == 'int' and type(arg) is not int:
|
|
raise TypeError()
|
|
elif (arg_type == 'float' or arg_type == 'double') and type(arg) is not float and type(
|
|
arg) is not int:
|
|
raise TypeError()
|
|
else:
|
|
raise TypeError()
|
|
|
|
except TypeError:
|
|
raise TypeError(
|
|
'The resource "{0}" requires exactly {1} argument(s) of type {2}.'.format(resource, len(arg_types),
|
|
arg_types))
|
|
|
|
|
|
def parse_resources(url):
|
|
import re
|
|
from ast import literal_eval
|
|
result = {}
|
|
resources = send_request(url, 'resourceNames', 'get')
|
|
resources = literal_eval(resources)
|
|
|
|
for resource in resources:
|
|
try:
|
|
# TODO - handle java arrays, ie, []
|
|
m = re.match(r"(\w+)\[?\]? (\w+)(\([\w ,]*\))?", resource).groups()
|
|
except AttributeError as e:
|
|
print(e)
|
|
continue
|
|
args = ''
|
|
|
|
if m[2] is not None:
|
|
args = tuple(m[2].replace(' ', '').replace('(', '').replace(')', '').split(','))
|
|
result[m[1]] = (m[0],) + args
|
|
else:
|
|
result[m[1]] = (m[0],)
|
|
if config.DEBUG:
|
|
print(f'Key: {m[1]} - {m[0]} -- {args}')
|
|
|
|
return result
|
|
|
|
|
|
def send_request(url, resource, method, args=None, body=None):
|
|
from requests import request
|
|
from requests.exceptions import ConnectionError, RequestException
|
|
|
|
if not type(url) is str:
|
|
raise TypeError('URL should be a string, found {0}'.format(type(url)))
|
|
if not type(resource) is str:
|
|
raise TypeError('URL should be a string, found {0}'.format(type(resource)))
|
|
|
|
kwargs = {}
|
|
kwargs.setdefault('allow_redirects', True)
|
|
kwargs.setdefault('headers', {'accept': 'application/json', 'content-type': 'application/json'})
|
|
|
|
if not url.endswith('/'):
|
|
url += '/'
|
|
|
|
full_url = url + resource + '/'
|
|
|
|
if args is not None and len(args) > 0:
|
|
full_url += '/'.join(tuple(str(x) for x in args))
|
|
|
|
if config.DEBUG:
|
|
print(f'Calling: {full_url}')
|
|
|
|
try:
|
|
response = request(method, full_url, data=body, **kwargs)
|
|
except ConnectionError as e:
|
|
print('Unable to connect to host: {0}.'.format(url))
|
|
raise e
|
|
except RequestException as e:
|
|
print('Request error from host: {0}.'.format(full_url))
|
|
raise e
|
|
|
|
if 200 <= response.status_code < 300:
|
|
result = response.text
|
|
elif response.status_code == 404:
|
|
raise ConnectionError(
|
|
'Resource not found on host or arguments are not formatted correctly: {0}'.format(response.text))
|
|
elif response.status_code == 405:
|
|
raise ConnectionError('Method not allowed on host: \n {0}'.format(response.text))
|
|
elif response.status_code == 500:
|
|
from pprint import pprint
|
|
# TODO: Handle exception
|
|
print('Exception on server:')
|
|
pprint(response.json())
|
|
raise
|
|
else:
|
|
raise ConnectionError(
|
|
'Unable to successfully connect to host. Returned with HTTP status code {0}.\n Content: {1}'.format(
|
|
response.status_code, response.text))
|
|
|
|
if config.DEBUG:
|
|
print(f'Succesfully called {full_url}')
|
|
print(f'Returned: {result}')
|
|
|
|
return result
|