46045-syslab/syslab/core/SyslabUnit.py

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("(\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