Package: openpaygo
OpenPAYGO - Python LibThis repository contains the Python library for using implementing the different OpenPAYGOToken Suite technologies on your server (for generating tokens and decoding openpaygo metrics payloads) or device (for decoding tokens and making openpaygo metrics payloads).CreditsThis open-source project was developped by Solaris Offgrid. Sponsorship for the original OpenPAYGO Token implementation was provided by EnAccess and sponsorphip for OpenPAYGO Metrics was provided by Solaris Offgrid.Table of ContentsKey FeaturesInstalling the libraryGetting Started - OpenPAYGO TokenGenerating Tokens (Server Side)Decoding Tokens (Device Side)Getting Started - OpenPAYGO MetricsGenerating a Request (Device Side)Handling a Request and Generating a Response (Server Side)ChangelogKey FeaturesImplements token generation and decoding with full support for the v2.3 of theOpenPAYGO Tokenspecifications.Implements payload authentication (verification + signing) and conversion from simple to condensed payload (and back) with full support of the v1.0-rc1 of theOpenPAYGO Metricsspecifications.Installing the libraryYou can install the library by runningpip install openpaygoor addingopenpaygoin your requirements.txt file and runningpip install -r requirements.txt.Getting Started - OpenPAYGO TokenGenerating Tokens (Server Side)You can use thegenerate_token()function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:secret_key(required): The secret key of the device. Must be passed as an hexadecimal string with 32 characters (e.g.dac86b1a29ab82edc5fbbc41ec9530f6).count(required): The token count used to make the last token.value(optional): The value to be passed in the token (typically the number of days of activation). Optional if thetoken_typeis Disable PAYG or Counter Sync. The value must be between 0 and 995.token_type(optional): Used to set the type of token (default is Add Time). Token types can be found in theTokenTypeclass: ADD_TIME, SET_TIME, DISABLE_PAYG, COUNTER_SYNC.starting_code(optional): If not provided, it is generated according to the method defined in the standard (SipHash-2-4 of the key, transformed to digit by the same method as the token generation).value_divider(optional): The dividing factor used for the value.restricted_digit_set(optional): If set totrue, the the restricted digit set will be used (only digits from 1 to 4).extended_token(optional): If set totruethen a larger token will be generated, able to contain values up to 999999. This is for special use cases of each device, such as settings change, and is not set in the standard.The function returns theupdated_countas a number as well as thetokenas a string, in that order. The function will raise aValueErrorif the key is in the wrong format or the value invalid.Example 1 - Add 7 days:fromopenpaygoimportgenerate_tokenfrommyexampleprojectimportdevice_getter# We get a device with the parameters we need from our database, this will be specific to your projectdevice=device_getter(serial=1234)# We get the new token and update the countdevice.count,new_token=generate_token(secret_key=device.secret_key,value=7,count=device.count)print('Token: '+new_token)device.save()# We save the new count that we set for the deviceExample 2 - Disable PAYG (unlock forever):fromopenpaygoimportgenerate_token,TokenType...# We get the new token and update the countdevice.count,new_token=generate_token(secret_key=device.secret_key,token_type=TokenType.DISABLE_PAYG,count=device.count)print('Token: '+new_token)device.save()# We save the new count that we set for the deviceDecoding Tokens (Device Side)You can use thedecode_token()function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:token(required): The token that was given by the user, as a stringsecret_key(required): The secret key of the device as a string with 32 hexadecimal characters (e.g.dac86b1a29ab82edc5fbbc41ec9530f6)count(required): The token count of the last valid token. When a device is new, this is 1.used_counts(optional): An array of recently used token counts, as returned by the function itself after the last valid token was decoded. This allows for handling unordered token entry.starting_code(optional): If not provided, it is generated according to the method defined in the standard (SipHash-2-4 of the key, transformed to digit by the same method as the token generation).value_divider(optional): The dividing factor used for the value.restricted_digit_set(optional): If set totrue, the the restricted digit set will be used (only digits from 1 to 4).The function returns the following variable in this order:value: The value associated with the token (if the token is ADD_TIME or SET_TIME).token_type: The type of the token that was provided. Token types can be found in theTokenTypeclass: ADD_TIME, SET_TIME, DISABLE_PAYG, COUNTER_SYNC or ALREADY_USED (if the token is valid but already used), INVALID (if the token was invalid).updated_count: The token count of the token, if it was valid.updated_used_counts: The updated array of recently used token, if the token was valid.The function will raise aValueErrorif the key is in the wrong format, but will not raise an error if the token is invalid (as it is a common expected behaviour), to check the validity of the token you must check the returntoken_typeand proceed accordingly depending on the type of token.Example:fromopenpaygoimportdecode_token# We assume the users enters a token and that the device state is saved in my_device_state...# We decode the tokenvalue,token_type,updated_count,updated_used_counts=decode_token(token=token_input,secret_key=my_device_state.secret_key,count=my_device_state.count,used_counts=my_device_state.used_counts)# If the token is valid, we update our count in the device stateiftoken_typenotin[TokenType.ALREADY_USED,TokenType.INVALID]:my_device_state.count=updated_countmy_device_state.used_counts=updated_used_counts# We perform the appropriate behaviour based on the token dataiftoken_type==TokenType.ADD_TIME:my_device_state.days_remaining+=valueprint(f'Added{value}days remaining')eliftoken_type==TokenType.SET_TIME:my_device_state.days_remaining=valueprint(f'Set to{value}days remaining')eliftoken_type==TokenType.DISABLE_PAYG:print('Unlocked Device')my_device_state.unlocked_forever=Trueeliftoken_type==TokenType.COUNTER_SYNC:print('Counter Synced')eliftoken_type==TokenType.ALREADY_USED:print('Token was already used')eliftoken_type==TokenType.INVALID:print('Token is invalid')Getting Started - OpenPAYGO MetricsGenerating a Request (Device Side)You can use theMetricsRequestHandlerobject to create a new OpenPAYGO Metrics request from start to finish. It accepts the following initial inputs:serial_number(required): The serial number of the devicedata_format(optional): The data format, provided as dictionnary matching the data format object specifications.secret_key(optional): The secret key provided as a string containing 32 hexadecimal characters (e.g.dac86b1a29ab82edc5fbbc41ec9530f6). Required ifauth_methodis defined.auth_method(optional): One of the auth method contained in theAuthMethodclass.It provides the following methods:set_timestamp(timestamp): Used to set thetimestampof the request.set_request_count(request_count): Used to set therequest_countof the request.set_data(data): Used to set thedataof the request, should be set in simple format as a dictionnay.set_historical_data(data): Used to set thehistorical_dataof the request, should be set in simple format as a dictionnary. The data is assumed to be separated by thehistorical_data_intervalunless an explicit timestamp is provided.get_simple_request_payload(): Returns the payload in simple format as a string containing JSON and including the authentication signature.get_condensed_request_payload(): Returns the payload in condensed format as a string containing JSON and including the authentication signature. It requiresdata_formatto be set. The data is automatically condensed from the set data and the data format and the signature is automatically generated.Example - Full Request flow from device side:fromopenpaygoimportMetricsRequestHandler,AuthMethodimportrequests# We assume the users enters a token and that the device state is saved in my_device_state...metrics_request=MetricsRequestHandler(serial_number=my_device_state.serial_number,secret_key=my_device_state.secret_key,data_format=my_device_state.data_format,auth_method=AuthMethod.RECURSIVE_DATA_AUTH)metrics_request.set_timestamp(1611583070)metrics_request.set_data({"token_count":3,"tampered":False,"firmware_version":"1.2.3"})# Here we assume that the data we send is separated by 60 seconds as per the data formatmetrics_request.set_historical_data([{"panel_voltage":12.31,"battery_voltage":12.32,"panel_current":1.23,"battery_current":-1.23,},{"panel_voltage":12.30,"battery_voltage":12.31,"panel_current":1.22,"battery_current":-1.21,}])payload=metrics_request.get_condensed_request_payload()# We can now proceed to send the payload to the URL# It looks something like `{"sn":"aaa111222","df":1234,"ts":1611583070,"d":[3,false,"1.2.3"],"hd":[[12.31,12.32,1.23,-1.23],[12.3,12.31,1.22,-1.21]],"a":"raa5cb1fda302cf94e"}`response=requests.post('https://<base_url>/dd',data=payload,headers={'Content-Type':'application/json'})try:response.json().get('tkl',[])fortokensintkl:# Here we decode the tokens received from the server and apply them (see example above)...Handling a Request and Generating a Response (Server Side)You can use theMetricsResponseHandlerobject to process your OpenPAYGO Metrics request from start to finish. It accepts the following initial inputs:metrics_payload(required): The OpenPAYGO Metrics payload, as a string containing the JSON payload.secret_key(optional): The secret key provided as a string containing 32 hexadecimal characters (e.g.dac86b1a29ab82edc5fbbc41ec9530f6). If the secret key is not set later usingset_device_parameters(), then you will not be able to verify the auth of the request (it will throw an error if you try) and the response will not be signed.skip_authdata_format(optional): The data format, provided as dictionnary matching the data format object specifications.last_request_count(optional): The request count of the last valid request (used for avoiding request replay)last_request_timestamp(optional): The timestamp of the last valid request (used for avoiding request replay)It provides the following methods:get_device_serial(): Returns the serial number of the device as a string.set_device_parameters(secret_key, data_format, last_request_count, last_request_timestamp): Used to set the device data required for proper processing of the request in the handler if it was not set initially, which is often the case as the serial number is usually required to fetch that data. It will returnValueErrorif either of the parameters is invalid.is_auth_valid(): Returnstrueif the authentication provided is valid orfalseif not. Note that it checks both that the signature is valid and that therequest_countortimestampare more recent than the one provided in the device parameters.get_simple_metrics(): Returns the metrics provided in the simple expanded format. It will also convert relative timestamps into explicit timestamps for easier processing.get_data_timestamp(): Returns the timestamp of the data, either thedata_collection_timestampif available or the timestamptimestampor the time of the request as fallback.get_request_timestamp(): This is thetimestampthat was explicitely set in the request and was signed, used to avoid replay attacks. It might be different from the data timestamp.get_request_count(): This is therequest_countset in the request and was signed, used to avoid replay attacks.get_token_count(): Returns the token count provided in the request (if any).expects_token_answer(): Returntrueif the payload requested tokens in the answer. You can set the tokens to be returned by callingadd_tokens_to_answer(token_list)withtoken_listbeing a list of token strings.expects_time_answer(): Returntrueif the payload requested either relative time or absolute time in the answer. You can set the time to be returned by callingadd_time_to_answer(target_datetime)withtarget_datetimebeing a datetime object. The function will automatically provide it in the correct format based on the request.add_settings_to_answer(settings_dict): Will add the provided settings dictionnary to the answer.add_extra_data_to_answer(extra_data_dict): Will add the provided extra data dictionnary to the answer.add_new_base_url_to_answer(new_base_url): Will tell the device to change the base URL to send the data to.get_answer_payload(): Will return the answer as a string based on the request and the data added to answer, it will automatically handle the authentication and fomatting.Example - Full Request flow from server side:fromopenpaygoimportMetricsResponseHandlerfrommy_db_serviceimportget_device,get_data_format,store_metric,get_pending_tokens@app.route('/dd')defdevice_data():# We load the metricstry:metrics=MetricsResponseHandler(request.data)exceptValueErrorase:return{'error':'Invalid data format'},400# We get the serial number and load the device data from our storagedevice=get_device(serial=metrics.get_device_serial())# We get the data format if needed from our storagedata_format=get_data_format(id=metrics.get_data_format_id())ifmetrics.get_data_format_id()elseNone# We set the device parameters in the metrics handlermetrics.set_device_parameters(secret_key=device.secret_key,data_format=data_format,last_request_count=device.last_request_count,last_request_timestamp=device.last_request_timestamp)# We check the authenticationifnotmetrics.is_auth_valid():return{'error':'Invalid authentication'},403# We transform the condensed data received from the device in simple datasimple_data=metrics.get_simple_metrics()# We store the metrics in our databaseformetric_name,metric_valueinsimple_data.get('data'):store_metric(name=metric_name,value=metric_value,time=metrics.get_data_timestamp())# We store the historical metrics as wellfortime_stepinsimple_data.get('historical_data'):# Here the handler automatically computed the timestamp for each steptimestamp=timestep.pop('timestamp')formetric_name,metric_valueintime_step:store_metric(name=metric_name,value=metric_value,time=timestamp)# We prepare the answerifmetrics.expects_token_answer():metrics.add_tokens_to_answer(get_pending_tokens(device,metrics.get_token_count()))elifmetrics.expects_time_answer():metrics.add_time_to_answer(device.expiration_datetime)# We can add extra datametrics.add_settings_to_answer({'language':'fr-FR'})# We update the request timestamp or the count if provided to be able to reject duplicate or replay requestsifmetrics.get_request_count():device.last_request_count=metrics.get_request_count()ifmetrics.get_request_timestamp():device.last_request_timestamp=metrics.get_request_timestamp()# The handler handles the signature, etc.returnmetrics.get_answer_payload(),200Changelog2023-10-13 - v0.5.4Safe handling of datetime before UNIX timestamp minimum when generating answer timestamp2023-10-13 - v0.5.3Fix handling oflast_request_timestampwhen checking auth2023-10-12 - v0.5.2Clarification in the doc of the behaviour whensecret_keyis missingImplemented coherent behaviour whensecret_keyis missing2023-10-12 - v0.5.1Fixes typo in function name2023-10-12 - v0.5.0Added convenience functions for accessing the current request count and request timestampImproved documentation on how to avoid replay attacks2023-10-12 - v0.4.0Added convenience functions for accessing token count and data timestampAdded automatic verification of last request count or timestamp during authFixed issues in documentation2023-10-09 - v0.3.0Fix token generation issueAdd support for OpenPAYGO Metrics Request GenerationAdd support for OpenPAYGO Metrics Request DecodingAdd support for OpenPAYGO Metrics Response Generation2023-10-03 - v0.2.0First working version published on PyPIHas support for OpenPAYGO TokenHas working CI for pushing to PyPI