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