diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/README.rst b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/README.rst index 218b1dba8d..4da6ce77a3 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/README.rst +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/README.rst @@ -12,7 +12,7 @@ activated environment: :: - pip install suds-jurko + pip install zeep Alternatively requirements can be installed from requirements.txt using: diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/__init__.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/__init__.py index d9a90e0482..5885c6fa61 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/__init__.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/__init__.py @@ -41,12 +41,11 @@ import logging import abc import sys - from . import service as cps -from . import async_service as async - +from . import async_service as async_service from .. import BaseInterface, BaseRegister, BasicRevert, DriverInterfaceError -from suds.sudsobject import asdict +#from suds.sudsobject import asdict +from zeep.helpers import serialize_object _log = logging.getLogger(__name__) @@ -68,7 +67,7 @@ point_name_mapping = {"Status.TimeStamp": "TimeStamp"} service = {} -gevent.spawn(async.web_service) +gevent.spawn(async_service.web_service) def recursive_asdict(d): @@ -78,7 +77,7 @@ def recursive_asdict(d): http://stackoverflow.com/questions/2412486/serializing-a-suds-object-in-python """ out = {} - for k, v in asdict(d).items(): + for k, v in serialize_object(d, dict).items(): if hasattr(v, '__keylist__'): out[k] = recursive_asdict(v) elif isinstance(v, list): @@ -153,6 +152,16 @@ def read_only_check(self): raise IOError("Trying to write to a point configured read only: {0}".format(self.attribute_name)) return True + def get_last_non_none_value(self,lst): + """ + Depends on port number, the result could be a list with None value + get last non-None value as result + """ + for item in reversed(lst): + if item is not None: + return item + return None + def get_register(self, result, method, port_flag=True): """Gets correct register from API response. @@ -165,9 +174,10 @@ def get_register(self, result, method, port_flag=True): :return: Correct register value cast to appropriate python type. Returns None if there is an error. """ try: - value = getattr(result, self.attribute_name)(self.port)[0] \ + _log.debug(f'In get_register, to get {self.attribute_name}, the port_flag is {port_flag}') + value = self.get_last_non_none_value(getattr(result, self.attribute_name)(self.port)) \ if port_flag \ - else getattr(result, self.attribute_name)(None)[0] + else self.get_last_non_none_value(getattr(result, self.attribute_name)(None)) return self.sanitize_output(self.data_type, value) except cps.CPAPIException as exception: if exception._responseCode not in ['153']: @@ -210,7 +220,7 @@ def __init__(self, read_only, point_name, attribute_name, units, data_type, stat def value(self): global service method = service[self.username].getStations - result = async.CPRequest.request(method, self.timeout, stationID=self.station_id) + result = async_service.CPRequest.request(method, self.timeout, stationID=self.station_id) result.wait() return self.get_register(result.value, method) @@ -251,7 +261,7 @@ def __init__(self, read_only, point_name, attribute_name, units, data_type, stat def value(self): global service method = service[self.username].getLoad - result = async.CPRequest.request(method, self.timeout, stationID=self.station_id) + result = async_service.CPRequest.request(method, self.timeout, stationID=self.station_id) result.wait() return self.get_register(result.value, method) @@ -277,7 +287,7 @@ def value(self, x): kwargs = {'stationID': self.station_id} if self.attribute_name == 'shedState' and not value: method = service[self.username].clearShedState - result = async.CPRequest.request(method, 0, stationID=self.station_id) + result = async_service.CPRequest.request(method, 0, stationID=self.station_id) elif self.attribute_name == 'shedState': _log.error('shedState may only be written with value 0. If you want to shedLoad, write to ' 'allowedLoad or percentShed') @@ -287,7 +297,7 @@ def value(self, x): kwargs[self.attribute_name] = value if self.port: kwargs['portNumber'] = self.port - result = async.CPRequest.request(method, 0, **kwargs) + result = async_service.CPRequest.request(method, 0, **kwargs) result.wait() if result.value.responseCode != "100": @@ -336,7 +346,7 @@ def value(self): if self.port: kwargs['portNumber'] = self.port - result = async.CPRequest.request(method, self.timeout, **kwargs) + result = async_service.CPRequest.request(method, self.timeout, **kwargs) result.wait() return self.get_register(result.value, method, False) @@ -362,7 +372,7 @@ def value(self, x): if self.attribute_name == 'clearAlarms' and value: kwargs = {'stationID': self.station_id} method = service[self.username].clearAlarms - result = async.CPRequest.request(method, 0, **kwargs) + result = async_service.CPRequest.request(method, 0, **kwargs) result.wait() if result.value.responseCode not in ['100', '153']: @@ -397,11 +407,12 @@ def __init__(self, read_only, point_name, attribute_name, units, data_type, stat def value(self): global service method = service[self.username].getChargingSessionData - result = async.CPRequest.request(method, self.timeout, stationID=self.station_id) + result = async_service.CPRequest.request(method, self.timeout, stationID=self.station_id) result.wait() # Of Note, due to API limitations, port number is ignored for these calls - return self.get_register(result.value, method, False) + # NOTE: Change this port number for Chargingsession data. + return self.get_register(result.value, method) @value.setter def value(self, x): @@ -432,7 +443,7 @@ def __init__(self, read_only, point_name, attribute_name, units, data_type, stat def value(self): global service method = service[self.username].getStationStatus - result = async.CPRequest.request(method, self.timeout, self.station_id) + result = async_service.CPRequest.request(method, self.timeout, self.station_id) result.wait() return self.get_register(result.value, method) @@ -469,7 +480,7 @@ def __init__(self, read_only, point_name, attribute_name, units, data_type, stat def value(self): global service method = service[self.username].getStationRights - result = async.CPRequest.request(method, self.timeout, stationID=self.station_id) + result = async_service.CPRequest.request(method, self.timeout, stationID=self.station_id) result.wait() # Note: this does not go through get_register, as it is of a unique type, 'dictionary.' @@ -572,7 +583,7 @@ def parse_config(self, config_dict, registry_config_str): description=description, port_number=port_num, username=config_dict['username'], - timeout=config_dict['cacheExpiration'] + timeout=config_dict.get('cacheExpiration',0) ) self.insert_register(register) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/async_service.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/async_service.py index 670c190aa5..95a2511558 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/async_service.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/async_service.py @@ -61,15 +61,14 @@ import gevent.event import gevent.queue import logging -import suds +import zeep from gevent import monkey from .service import CPAPIException from datetime import datetime, timedelta -monkey.patch_all() _log = logging.getLogger(__name__) -SERVICE_WSDL_URL = "https://webservices.chargepoint.com/cp_api_5.0.wsdl" +SERVICE_WSDL_URL = "https://webservices.chargepoint.com/cp_api_5.1.wsdl" # Queue for Web API requests and responses. It is managed by the long running # web_service() greenlet. web_service_queue = gevent.queue.Queue() @@ -267,7 +266,7 @@ def web_service(): web_cache[item_key] = cache_item if not client_set: - client_set.add(suds.client.Client(SERVICE_WSDL_URL)) + client_set.add(zeep.Client(SERVICE_WSDL_URL)) client = client_set.pop() gevent.spawn(web_call, item, client) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/credential_check.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/credential_check.py index 9687d08d63..e06c51b25d 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/credential_check.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/credential_check.py @@ -1,5 +1,5 @@ from . import service as cps -import suds +import zeep import io station_csv = { @@ -176,5 +176,5 @@ else: print("Some other error happened") - except suds.WebFault as a: + except zeep.exception.Fault as e: print("Sorry, your API credentials are invalid. Please contact Chargepoint for assistance.") diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/requirements.txt b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/requirements.txt index 274ad9e069..f61b24a644 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/requirements.txt +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/requirements.txt @@ -1 +1 @@ -suds-jurko==0.6 \ No newline at end of file +zeep==4.2.1 \ No newline at end of file diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/service.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/service.py index 916333a9ed..49884e6e7d 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/service.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/service.py @@ -36,14 +36,14 @@ # under Contract DE-AC05-76RL01830 # }}} -import suds.client -import suds.wsse +import zeep +from zeep.wsse.username import UsernameToken +from zeep import Settings import logging logger = logging.getLogger('chargepoint') -SERVICE_WSDL_URL = "https://webservices.chargepoint.com/cp_api_5.0.wsdl" - +SERVICE_WSDL_URL = "https://webservices.chargepoint.com/cp_api_5.1.wsdl" CPAPI_SUCCESS = '100' XMPP_EVENTS = [ @@ -170,13 +170,13 @@ class CPStation: """Wrapper around the getStations() return by Chargepoint API. Data surrounding a Chargepoint Station can generally be categorized as static or dynamic. Chargepoint API has two - basic calls, getLoad and getStation, that each return station data. getLoad returns the stationLoadData SUDS - object, and getStation returns the stationDataExtended SUDS object. These are each kept as separate meta-data + basic calls, getLoad and getStation, that each return station data. getLoad returns the stationLoadData object, + and getStation returns the stationDataExtended object. These are each kept as separate meta-data parameters. :param cps: Chargepoint Service object. - :param sld: stationLoadData SUDS object. - :param sde: stationDataExtended SUDS object. + :param sld: stationLoadData object. + :param sde: stationDataExtended object. (stationDataExtended){ stationID = "1:00001" @@ -430,8 +430,17 @@ def get_port_value(port_number, data, attribute): if flag: logger.warning("Station does not have a definition for port {0}".format(port_number)) else: - logger.warning("Response does not have Ports defined") - return None + if (attribute in ['sessionID', 'startTime', 'endTime', 'Energy', 'rfidSerialNumber', 'driverAccountNumber', + 'driverName']) and int(data['portNumber']) == port_number: + try: + data_attribute = data[attribute] + return data_attribute + except: + logger.warning(f'Response does not have {attribute} field') + return None + else: + logger.warning("Response does not have Ports defined") + return None @staticmethod def check_output(attribute, parent_dict): @@ -455,6 +464,7 @@ def get_attr_from_response(name_string, response, portNum=None): else CPAPIResponse.is_not_found(name_string)) else: list.append(CPAPIResponse.get_port_value(portNum, item, name_string)) + logger.debug(f'{name_string} list for {portNum} is {list}') return list @@ -646,12 +656,14 @@ def Type(self, port=None): def startTime(self, port=None): if port: + logger.debug(f'startTime port is {port}') return CPAPIResponse.get_attr_from_response('startTime', self.stations, port) else: return [self.pricing_helper('startTime', station) for station in self.stations] def endTime(self, port=None): if port: + logger.debug(f'endTime port is {port}') return CPAPIResponse.get_attr_from_response('endTime', self.stations, port) else: return [self.pricing_helper('endTime', station) for station in self.stations] @@ -772,8 +784,8 @@ class CPService: """ Python wrapper around the Chargepoint WebServices API. - Current Version: 5.0 - Docs: ChargePoint_Web_Services_API_Guide_Ver4.1_Rev5.pdf + Current Version: 5.1 + Docs: ChargePoint_Web_Services_API_Guide_Ver5.1_Rev1.13.pdf """ def __init__(self, username=None, password=None): @@ -783,18 +795,18 @@ def __init__(self, username=None, password=None): """ self._username = username self._password = password - self._suds_client = None + self._zeep_client = None @property def _client(self): - """Initialize the SUDS client if necessary.""" + """Initialize the ZEEP client if necessary.""" - if self._suds_client is None: - self._suds_client = suds.client.Client(SERVICE_WSDL_URL) + if self._zeep_client is None: + self._zeep_client = zeep.Client(SERVICE_WSDL_URL) # Add SOAP Security tokens self.set_security_token() - return self._suds_client + return self._zeep_client @property def _soap_service(self): @@ -802,13 +814,13 @@ def _soap_service(self): def set_security_token(self): # Add SOAP Security tokens - security = suds.wsse.Security() - token = suds.wsse.UsernameToken(self._username, self._password) - security.tokens.append(token) - self._suds_client.set_options(wsse=security) + #TODO:might need to put this in config + #NOTE: wihtout this setting, zeep will not get result + settins = Settings(strict=False, xml_huge_tree=True, xsd_ignore_sequence_order=True) + self._zeep_client = zeep.Client(SERVICE_WSDL_URL, wsse=UsernameToken(self._username, self._password),settings=settins) def set_client(self, client): - self._suds_client = client + self._zeep_client = client self.set_security_token() def clearAlarms(self, **kwargs): @@ -834,7 +846,7 @@ def clearAlarms(self, **kwargs): :returns SOAP reply object. If successful, there will be a responseCode of '100'. """ - searchQuery = self._client.factory.create('clearAlarmsSearchQuery') + searchQuery = self._client.get_type('ns0:clearAlarmsSearchQuery')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.clearAlarms(searchQuery) @@ -850,7 +862,7 @@ def clearShedState(self, **kwargs): :returns SOAP reply object. If successful, there will be a responseCode of '100'. """ - searchQuery = self._client.factory.create('shedQueryInputData') + searchQuery = self._client.get_type('ns0:shedQueryInputData')() if 'stationID' in kwargs.keys(): setattr(searchQuery, 'shedStation', {'stationID': kwargs['stationID']}) elif 'sgID' in kwargs.keys(): @@ -904,7 +916,7 @@ def getAlarms(self, **kwargs): } """ - searchQuery = self._client.factory.create('getAlarmsSearchQuery') + searchQuery = self._client.get_type('ns0:getAlarmsSearchQuery')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getAlarms(searchQuery) @@ -979,7 +991,7 @@ def getChargingSessionData(self, **kwargs): } """ - searchQuery = self._client.factory.create('sessionSearchdata') + searchQuery = self._client.get_type('ns0:sessionSearchdata')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getChargingSessionData(searchQuery) @@ -1032,7 +1044,8 @@ def getLoad(self, **kwargs): """ # @ToDo: Figure out what type of request searchQuery should be here. - searchQuery = self._client.factory.create('stationSearchRequestExtended') + # @Note: Looks like it should be {sgID: xsd:int, stationID: xsd:string, sessionID: xsd:long} + searchQuery = {} for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getLoad(searchQuery) @@ -1073,7 +1086,7 @@ def getOrgsAndStationGroups(self, **kwargs): } """ - searchQuery = self._client.factory.create('getOrgsAndStationGroupsSearchQuery') + searchQuery = self._client.get_type('ns0:getOrgsAndStationGroupsSearchQuery')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getOrgsAndStationGroups(searchQuery) @@ -1228,7 +1241,7 @@ def getStationRights(self, **kwargs): } """ - searchQuery = self._client.factory.create('stationRightsSearchRequest') + searchQuery = self._client.get_type('ns0:stationRightsSearchRequest')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getStationRights(searchQuery) @@ -1373,7 +1386,8 @@ def getStations(self, **kwargs): moreFlag = 0 } """ - searchQuery = self._client.factory.create('stationSearchRequestExtended') + + searchQuery = self._client.get_type('ns0:stationSearchRequestExtended')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getStations(searchQuery) @@ -1460,7 +1474,7 @@ def getUsers(self, **kwargs): } """ - searchQuery = self._client.factory.create('getUsersSearchRequest') + searchQuery = self._client.get_type('ns0:getUsersSearchRequest')() for k, v in kwargs.items(): setattr(searchQuery, k, v) response = self._soap_service.getUsers(searchQuery) @@ -1498,7 +1512,8 @@ def shedLoad(self, **kwargs): :returns SOAP reply object. If successful, there will be a responseCode of '100'. """ - searchQuery = self._client.factory.create('shedLoadQueryInputData') + + searchQuery = self._client.get_type('ns0:shedLoadQueryInputData')() port = kwargs.pop('portNumber', None) query_params = {'stationID': kwargs['stationID']} if port: