From 2fa105100677ce20df9eed74046664f5da2fefd6 Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Thu, 10 Jan 2013 08:59:04 +0200 Subject: Initial commit --- __init__.py | 0 gsx.py | 387 ++++++++++++++++++++++++++++++++ requirements.pip | 1 + tests/acknowledge_communication.json | 5 + tests/create_app_order.json | 18 ++ tests/create_carryin_repair.json | 36 +++ tests/create_cnd_repair.json | 21 ++ tests/create_escalation.json | 11 + tests/create_srf_order.json | 9 + tests/create_stocking_order.json | 8 + tests/create_whole_unit_exchange.json | 32 +++ tests/fetch_communication_articles.json | 7 + tests/fetch_communication_content.json | 4 + tests/fetch_ios_activation.json | 3 + tests/parts_lookup.json | 3 + tests/parts_pending_return.json | 3 + tests/parts_return_update.json | 12 + tests/pending_app_orders.json | 4 + tests/register_bulk_return.json | 13 ++ tests/repair_lookup.json | 19 ++ tests/return_label.json | 4 + tests/return_report.json | 4 + tests/update_carryin_repair.json | 13 ++ tests/update_escalation.json | 8 + tests/update_serial_number.json | 12 + tests/warranty_status.json | 3 + 26 files changed, 640 insertions(+) create mode 100644 __init__.py create mode 100755 gsx.py create mode 100644 requirements.pip create mode 100644 tests/acknowledge_communication.json create mode 100644 tests/create_app_order.json create mode 100644 tests/create_carryin_repair.json create mode 100644 tests/create_cnd_repair.json create mode 100644 tests/create_escalation.json create mode 100644 tests/create_srf_order.json create mode 100644 tests/create_stocking_order.json create mode 100644 tests/create_whole_unit_exchange.json create mode 100644 tests/fetch_communication_articles.json create mode 100644 tests/fetch_communication_content.json create mode 100644 tests/fetch_ios_activation.json create mode 100644 tests/parts_lookup.json create mode 100644 tests/parts_pending_return.json create mode 100644 tests/parts_return_update.json create mode 100644 tests/pending_app_orders.json create mode 100644 tests/register_bulk_return.json create mode 100644 tests/repair_lookup.json create mode 100644 tests/return_label.json create mode 100644 tests/return_report.json create mode 100644 tests/update_carryin_repair.json create mode 100644 tests/update_escalation.json create mode 100644 tests/update_serial_number.json create mode 100644 tests/warranty_status.json diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsx.py b/gsx.py new file mode 100755 index 0000000..9654da3 --- /dev/null +++ b/gsx.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python +#coding=utf-8 + +import re +from suds.client import Client + +import logging +logging.basicConfig(level=logging.INFO) + +logging.getLogger('suds.client').setLevel(logging.DEBUG) + +CLIENT = None +SESSION = dict() # module-level variable + +def looks_like(value, what=None): + """ + Tries to guess the meaning of value + """ + result = None + + if not isinstance(value, basestring): + raise ValueError('%s is not valid input') + + rex = { + 'partNumber' : r'^([A-Z]{1,2})?\d{3}\-?(\d{4}|[A-Z]{2})(/[A-Z])?$', + 'serialNumber' : r'^[A-Z0-9]{11,12}$', + 'eeeCode' : r'^[A-Z0-9]{3,4}$', + 'returnOrder' : r'^7\d{9}$', + 'repairNumber' : r'^\d{12}$', + 'dispatchId' : r'^G\d{9}$', + 'alternateDeviceId' : r'^\d{15}$', + 'diagnosticEventNumber': r'^\d{23}$', + 'productName' : r'^i?Mac', + } + + for k, v in rex.items(): + if re.match(v, value): + result = k + + return (result == what) if what else result + +class GsxObject(object): + """ + The thing that gets sent to and from GSX + """ + dt = 'ns3:authenticateRequestType' # The GSX datatype matching this object + request_dt = 'ns3:authenticateRequestType' # The GSX datatype matching this request + method = 'Authenticate' # The SOAP method to call on the GSX server + + def __init__(self, *args, **kwargs): + self.data = kwargs + self.dt = CLIENT.factory.create(self.dt) + self.request_dt = CLIENT.factory.create(self.request_dt) + + def set_method(self, new_method): + self.method = new_method + + def set_type(self, new_dt): + """ + Sets the object's primary data type to new_dt + """ + self.dt = self.__make_type(new_dt) + + try: + for k, v in self.data.items(): + setattr(self.dt, k, v) + except Exception, e: + pass + + def set_request(self, new_dt=None, field=None): + """ + Sets the field of this object's request datatype to the new value + """ + if new_dt: + self.request_dt = self.__make_type(new_dt) + + setattr(self.request_dt, field, self.dt) + + def submit(self, method): + setattr(self.request_dt, 'userSession', SESSION) + + f = getattr(CLIENT.service, method) + result = f(self.request_dt) + return result + + def __make_type(self, new_dt): + return CLIENT.factory.create(new_dt) + +class Lookup(GsxObject): + def parts(self): + """ + The Parts Lookup API allows users to access part and part pricing data prior to + creating a repair or order. Parts lookup is also a good way to search for + part numbers by various attributes of a part + (config code, EEE code, serial number, etc.). + """ + dt = CLIENT.factory.create('ns0:partsLookupRequestType') + dt.userSession = SESSION + dt.lookupRequestData = self.data + result = CLIENT.service.PartsLookup(dt) + return result.parts + + def repairs(self): + dt = CLIENT.factory.create('ns6:repairLookupInfoType') + dt.serialNumber = self.data['serialNumber'] + request = CLIENT.factory.create('ns1:repairLookupRequestType') + request.userSession = SESSION + request.lookupRequestData = dt + result = CLIENT.service.RepairLookup(request) + return result.lookupResponseData + +class Diagnostics(GsxObject): + def fetch(self): + """ + The Fetch Repair Diagnostics API allows the service providers/depot/carriers + to fetch MRI/CPU diagnostic details from the Apple Diagnostic Repository OR + diagnostic test details of iOS Devices. + The ticket is generated within GSX system. + """ + if 'alternateDeviceId' in self.data: + self.set_type('ns7:fetchIOSDiagnosticRequestDataType') + self.set_request('ns3:fetchIOSDiagnosticRequestType', 'lookupRequestData') + result = self.submit('FetchIOSDiagnostic') + else: + self.set_request('ns3:fetchRepairDiagnosticRequestType', 'lookupRequestData') + +class Returns(GsxObject): + def get_report(self): + """ + The Return Report API returns a list of all parts that are returned + or pending for return, based on the search criteria. + """ + + def get_label(self): + """ + The Return Label API retrieves the Return Label for a given Return Order Number. + """ + + def get_proforma(self): + """ + The View Bulk Return Proforma API allows you to view + the proforma label for a given Bulk Return Id. + You can create a parts bulk return by using the Register Parts for Bulk Return API. + """ + + def register_parts(self): + """ + The Register Parts for Bulk Return API creates a bulk return for + the registered parts. + The API returns the Bulk Return Id with the packing list. + """ + + def get_pending(self): + """ + The Parts Pending Return API returns a list of all parts that + are pending for return, based on the search criteria. + """ + +class Part(GsxObject): + def lookup(self): + lookup = Lookup(**self.data) + return lookup.parts() + +class Escalation(GsxObject): + def create(self): + """ + The Create General Escalation API allows users to create + a general escalation in GSX. The API was earlier known as GSX Help. + """ + self.set_type('ns6:createGenEscRequestDataType') + self.set_request('ns1:createGenEscRequestType', 'escalationRequest') + result = self.submit('CreateGeneralEscalation') + return result.escalationConfirmation + + def update(self): + """ + The Update General Escalation API allows Depot users to + update a general escalation in GSX. + """ + self.set_type('ns6:updateGeneralEscRequestDataType') + self.set_request('ns1:updateGeneralEscRequestType', 'escalationRequest') + result = self.submit('UpdateGeneralEscalation') + return result.escalationConfirmation + +class Repair(GsxObject): + + dt = 'ns6:repairLookupInfoType' + request_dt = 'ns1:repairLookupRequestType' + + def __init__(self, *args, **kwargs): + super(Repair, self).__init__() + self.data = kwargs + + def create_carryin(self): + """ + The Parts Pending Return API returns a list of all parts that + are pending for return, based on the search criteria. + """ + self.set_type('ns2:emeaAspCreateCarryInRepairDataType') + ca = CLIENT.factory.create('ns7:addressType') + self.dt.customerAddress = self.data['customerAddress'] + self.set_request('ns2:carryInRequestType', 'repairData') + result = self.submit('CreateCarryInRepair') + print result + + def create_cnd(self): + """ + The Create CND Repair API allows Service Providers to create a repair + whenever the reported issue cannot be duplicated, and the repair + requires no parts replacement. + N01 Unable to Replicate + N02 Software Update/Issue + N03 Cable/Component Reseat + N05 SMC Reset + N06 PRAM Reset + N07 Third Party Part + N99 Other + """ + + def update_carryin(self): + """ + The Update Carry-In Repair API allows the service providers + to update the existing open carry-in repairs. + This API assists in addition/deletion of parts and addition of notes to a repair. + On successful update, the repair confirmation number and + quote for any newly added parts would be returned. + In case of any validation error or unsuccessful update, a fault code is issued. + + Carry-In Repair Update Status Codes: + AWTP Awaiting Parts + AWTR Parts Allocated + BEGR In Repair + RFPU Ready for Pickup + """ + pass + + def lookup(self): + """ + The Repair Lookup API mimics the front-end repair search functionality. + It fetches up to 2500 repairs in a given criteria. + Subsequently, the extended Repair Status API can be used + to retrieve more details of the repair. + """ + for k, v in self.data.items(): + setattr(self.dt, k, v) + + self.set_request(field='lookupRequestData') + results = self.submit('RepairLookup') + return results + + def mark_complete(self): + """ + The Mark Repair Complete API allows a single or an array of + repair confirmation numbers to be submitted to GSX to be marked as complete. + """ + pass + + def delete(self): + """ + The Delete Repair API allows the service providers to delete + the existing GSX Initiated Carry-In, Return Before Replace & Onsite repairs + which are in Declined-Rejected By TSPS Approver state, + that do not have an active repair id. + """ + pass + + def get_status(): + """ + The Repair Status API retrieves the status + for the submitted repair confirmation number(s). + """ + pass + + def get_details(self): + dt = CLIENT.factory.create('ns0:repairDetailsRequestType') + dt.dispatchId = self.data['dispatchId'] + dt.userSession = SESSION + results = CLIENT.service.RepairDetails(dt) + +class Communication(GsxObject): + def get_content(): + """ + The Fetch Communication Content API allows the service providers/depot/carriers + to fetch the communication content by article ID from the service news channel. + """ + + def get_articles(): + """ + The Fetch Communication Articles API allows the service partners + to fetch all the active communication message IDs. + """ + +class Product(GsxObject): + + dt = 'ns7:unitDetailType' + + def __init__(self, sn): + super(Product, self).__init__() + self.dt.serialNumber = sn + self.sn = sn + self.lookup = Lookup(serialNumber=self.sn) + + def get_model(self): + """ + This API allows Service Providers/Carriers to fetch + Product Model information for the given serial number. + """ + self.set_request('ns3:fetchProductModelRequestType', 'productModelRequest') + result = self.submit('FetchProductModel') + return result + + def get_warranty(self, date_received=None, parts=[]): + """ + The Warranty Status API retrieves the same warranty details + displayed on the GSX Coverage screen. + If part information is provided, the part warranty information is returned. + If you do not provide the optional part information in the + warranty status request, the unit level warranty information is returned. + """ + self.set_request('ns3:warrantyStatusRequestType', 'unitDetail') + result = self.submit('WarrantyStatus') + return result.warrantyDetailInfo + + def get_activation(self): + """ + The Fetch iOS Activation Details API is used to + fetch activation details of iOS Devices. + """ + dt = CLIENT.factory.create('ns3:fetchIOSActivationDetailsRequestType') + dt.serialNumber = self.sn + dt.userSession = SESSION + result = CLIENT.service.FetchIOSActivationDetails(dt) + return result.activationDetailsInfo + + def get_parts(self): + return self.lookup.parts() + + def get_repairs(self): + return self.lookup.repairs() + +def init(env='ut', region='emea'): + global CLIENT + + envs = ('pr', 'it', 'ut',) + hosts = {'pr': 'ws2', 'it': 'wsit', 'ut': 'wsut'} + + if env not in envs: + raise ValueError('Environment should be one of: %s' % ','.join(envs)) + + url = 'https://gsx{env}.apple.com/wsdl/{region}Asp/gsx-{region}Asp.wsdl' + url = url.format(env=hosts[env], region=region) + + CLIENT = Client(url) + +def connect(user_id, password, sold_to, lang='en', tz='CEST', + env='ut', region='emea'): + global SESSION + + SESSION = {} + + init(env, region) + + account = CLIENT.factory.create('ns3:authenticateRequestType') + + account.userId = user_id + account.password = password + account.languageCode = lang + account.userTimeZone = tz + account.serviceAccountNo = sold_to + + result = CLIENT.service.Authenticate(account) + SESSION['userSessionId'] = result.userSessionId + + return SESSION + +def logout(): + CLIENT.service.Logout() + +if __name__ == '__main__': + import json + import sys + #connect(*sys.argv[1:4]) + #f = 'tests/create_carryin_repair.json' + #f = 'tests/update_escalation.json' + #fp = open(f, 'r') + #data = json.load(fp) + \ No newline at end of file diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 0000000..55a2df3 --- /dev/null +++ b/requirements.pip @@ -0,0 +1 @@ +suds diff --git a/tests/acknowledge_communication.json b/tests/acknowledge_communication.json new file mode 100644 index 0000000..5fd7b9b --- /dev/null +++ b/tests/acknowledge_communication.json @@ -0,0 +1,5 @@ +{ + "acknowledgement": [ + {"articleID": "SN34", "acknowledgeType": "READ"} + ] +} diff --git a/tests/create_app_order.json b/tests/create_app_order.json new file mode 100644 index 0000000..1d1bba8 --- /dev/null +++ b/tests/create_app_order.json @@ -0,0 +1,18 @@ +{ + "shipTo": "672592", + "addressLine1": "1277 Jolly Avenue", + "country": "FI", + "zipCode": "00180", + "city": "Sunnyvale", + "state": "ZZ", + "firstName": "Test", + "lastName": "Apple", + "primaryPhone": "2086785671", + "emailAddress": "Test@Apple.com", + "productNumber": "8961", + "purchaseOrder": "Order8961", + "requestPart": { + "serialNumber": "" + }, + "pocLanguage": "FIN" +} diff --git a/tests/create_carryin_repair.json b/tests/create_carryin_repair.json new file mode 100644 index 0000000..085c765 --- /dev/null +++ b/tests/create_carryin_repair.json @@ -0,0 +1,36 @@ +{ + "shipTo": "677592", + "requestReviewByApple": "N", + "serialNumber": "C3TFJJTNDCP9", + "symptom": "Does not work", + "diagnosis": "Does not work", + "unitReceivedDate": "01/01/13", + "unitReceivedTime": "12:00 AM", + "checkIfOutOfWarrantyCoverage": "N", + "overrideDiagnosticCodeCheck": "Y", + "poNumber": "Order12341", + "orderLines": [ + { + "partNumber": "FD661-6136", + "comptiaCode": "T03", + "comptiaModifier": "B", + "abused": "Y", + "outOfWarrantyFlag": "N", + "diagnosticCode": "" + } + ], + "customerAddress": { + "addressLine1": "Address line 1", + "country": "FI", + "zipCode": "95014", + "regionCode": "005", + "city": "Cupertino", + "state": "ZZ", + "street": "Valley Green Dr", + "firstName": "First", + "lastName": "Last", + "companyName": "Apple Inc", + "primaryPhone": "4088887766", + "emailAddress": "abc@test.com" + } +} diff --git a/tests/create_cnd_repair.json b/tests/create_cnd_repair.json new file mode 100644 index 0000000..c46d8c1 --- /dev/null +++ b/tests/create_cnd_repair.json @@ -0,0 +1,21 @@ +{ + "shipTo": "677592", + "customerAddress": { + "addressLine1": "1277 Henderson Avenue", + "city": "Helsinki", + "country": "FI", + "firstName": "test", + "lastName": "Apple", + "primaryPhone": "2085673498", + "region": "004", + "state": "ZZ", + "zipCode": "00180", + "emailAddress": "test@Apple.com" + }, + "notes": "This is a test note", + "symptom": "This is atest symptom", + "serialNumber": "", + "unitReceivedDate": "10/01/12", + "unitReceivedTime": "12:00 AM", + "classification": "N01" +} diff --git a/tests/create_escalation.json b/tests/create_escalation.json new file mode 100644 index 0000000..3e1ac9c --- /dev/null +++ b/tests/create_escalation.json @@ -0,0 +1,11 @@ +{ + "shipTo": "677592", + "issueTypeCode": "PUR", + "notes": "This is a test", + "escalationContext": { + "contextType": "", "contextID": "" + }, + "attachment": [ + {"fileName": "", "fileData": ""} + ] +} diff --git a/tests/create_srf_order.json b/tests/create_srf_order.json new file mode 100644 index 0000000..7a58344 --- /dev/null +++ b/tests/create_srf_order.json @@ -0,0 +1,9 @@ +{ + "shipToCode": "677592", + "purchaseOrderNumber": "Order12345", + "serialNumber": "", + "reasonForOrder": "SR7", + "orderLines": [ + {"partNumber": "034-4498", "quantity": "1"} + ] +} diff --git a/tests/create_stocking_order.json b/tests/create_stocking_order.json new file mode 100644 index 0000000..1af23a4 --- /dev/null +++ b/tests/create_stocking_order.json @@ -0,0 +1,8 @@ +{ + "purchaseOrderNumber": "Order7899", + "shipToCode": "677592", + "orderLines": [ + {"partNumber": "661-5097", "quantity": "1"}, + {"partNumber": "661-5098", "quantity": "2"} + ] +} diff --git a/tests/create_whole_unit_exchange.json b/tests/create_whole_unit_exchange.json new file mode 100644 index 0000000..b7e77e2 --- /dev/null +++ b/tests/create_whole_unit_exchange.json @@ -0,0 +1,32 @@ +{ + "billTo": "", + "customerAddress": "", + "diagnosedByTechId": "", + "diagnosis": "", + "fileName": "", + "fileData": "", + "imeiNumber": "", + "requestReviewByApple": "", + "serialNumber": "", + "notes": "", + "orderLines": [ + { + "partNumber": "", + "comptiaCode": "", + "comptiaModifier": "", + "outOfWarrantyFlag": "", + "coveredByACPlus": "", + "diagnosticCode": "" + } + ], + "poNumber": "", + "popFaxed": "", + "referenceNumber": "", + "shipTo": "", + "shipBox": "", + "shipUnitToCustomer": "", + "symptom": "", + "unitReceivedDate": "", + "unitReceivedTime": "", + "notaFiscalNumber": "" +} diff --git a/tests/fetch_communication_articles.json b/tests/fetch_communication_articles.json new file mode 100644 index 0000000..d5a0674 --- /dev/null +++ b/tests/fetch_communication_articles.json @@ -0,0 +1,7 @@ +{ + "priority": "HIGH", + "fromDate": "", + "toDate": "", + "readStatus": "", + "productModel": {} +} diff --git a/tests/fetch_communication_content.json b/tests/fetch_communication_content.json new file mode 100644 index 0000000..cb28cb2 --- /dev/null +++ b/tests/fetch_communication_content.json @@ -0,0 +1,4 @@ +{ + "articleID": "SN234", + "languageCode": "en" +} diff --git a/tests/fetch_ios_activation.json b/tests/fetch_ios_activation.json new file mode 100644 index 0000000..4b73647 --- /dev/null +++ b/tests/fetch_ios_activation.json @@ -0,0 +1,3 @@ +{ + "serialNumber": "88028ELHA4S" +} diff --git a/tests/parts_lookup.json b/tests/parts_lookup.json new file mode 100644 index 0000000..b271e16 --- /dev/null +++ b/tests/parts_lookup.json @@ -0,0 +1,3 @@ +{ + "partNumber": "661-5732" +} diff --git a/tests/parts_pending_return.json b/tests/parts_pending_return.json new file mode 100644 index 0000000..91e8ad2 --- /dev/null +++ b/tests/parts_pending_return.json @@ -0,0 +1,3 @@ +{ + "repairType": "CA" +} diff --git a/tests/parts_return_update.json b/tests/parts_return_update.json new file mode 100644 index 0000000..5cdefee --- /dev/null +++ b/tests/parts_return_update.json @@ -0,0 +1,12 @@ +{ + "repairConfirmationNumber": "G135764009", + "notes": "", + "orderLines": [ + {"partNumber": "661-6529"}, + {"comptiaCode": "N01"}, + {"comptiaModifier": "A"}, + {"orderNumber": "11223344"}, + {"returnOrderNumber": ""}, + {"returnType": "1"} + ] +} diff --git a/tests/pending_app_orders.json b/tests/pending_app_orders.json new file mode 100644 index 0000000..ec49768 --- /dev/null +++ b/tests/pending_app_orders.json @@ -0,0 +1,4 @@ +{ + "startDate": "01/01/12", + "endDate": "10/02/12" +} diff --git a/tests/register_bulk_return.json b/tests/register_bulk_return.json new file mode 100644 index 0000000..3b6bda8 --- /dev/null +++ b/tests/register_bulk_return.json @@ -0,0 +1,13 @@ +{ + "shipToCode": "", + "carrierCode": "XUPSN", + "trackingNumber": "12341234", + "length": "10", + "width": "10", + "height": "10", + "estimatedTotalWeight": "20", + "notes": "", + "bulkReturnOrder": [ + {"returnOrderNumber": "7444640074", "partNumber": "661-6028", "boxNumber": "1"} + ] +} diff --git a/tests/repair_lookup.json b/tests/repair_lookup.json new file mode 100644 index 0000000..12f35a6 --- /dev/null +++ b/tests/repair_lookup.json @@ -0,0 +1,19 @@ +{ + "repairConfirmationNumber": "", + "customerEmailAddress": "", + "customerFirstName": "", + "customerLastName": "", + "fromDate": "", + "toDate": "", + "incompleteRepair": "", + "pendingShipment": "", + "purchaseOrderNumber": "", + "repairNumber": "", + "repairStatus": "", + "repairType": "", + "serialNumber": "W874939YX92", + "shipToCode": "", + "technicianFirstName": "", + "technicianLastName": "", + "unreceivedModules": "" +} diff --git a/tests/return_label.json b/tests/return_label.json new file mode 100644 index 0000000..edebf48 --- /dev/null +++ b/tests/return_label.json @@ -0,0 +1,4 @@ +{ + "returnOrderNumber": "7438971408", + "partNumber": "NF661-5769" +} diff --git a/tests/return_report.json b/tests/return_report.json new file mode 100644 index 0000000..a868dd3 --- /dev/null +++ b/tests/return_report.json @@ -0,0 +1,4 @@ +{ + "shipTo": "", + "warrantyType": "IW" +} diff --git a/tests/update_carryin_repair.json b/tests/update_carryin_repair.json new file mode 100644 index 0000000..90ee39e --- /dev/null +++ b/tests/update_carryin_repair.json @@ -0,0 +1,13 @@ +{ + "repairConfirmationNumber": "G135773004", + "statusCode": "BEGR", + "technicianId": "", + "notes": "", + "orderLines": [ + { + "partNumber": "661-5594", + "comptiaCode": "F1A", + "comptiaModifier": "B" + } + ] +} diff --git a/tests/update_escalation.json b/tests/update_escalation.json new file mode 100644 index 0000000..abe3474 --- /dev/null +++ b/tests/update_escalation.json @@ -0,0 +1,8 @@ +{ + "escalationId": "1599790", + "notes": "This is an update", + "status": "", + "attachment": [ + {"fileName": "", "fileData": ""} + ] +} diff --git a/tests/update_serial_number.json b/tests/update_serial_number.json new file mode 100644 index 0000000..789bdf6 --- /dev/null +++ b/tests/update_serial_number.json @@ -0,0 +1,12 @@ +{ + "repairConfirmationNumber": "G135762375", + "partInfo": [ + { + "partNumber": "661-4964", + "oldSerialNumber": "W882300FK22YA", + "serialNumber": "W8829012R22XA", + "reasonCode": "", + "isPartDOA": "N" + } + ] +} diff --git a/tests/warranty_status.json b/tests/warranty_status.json new file mode 100644 index 0000000..1c51713 --- /dev/null +++ b/tests/warranty_status.json @@ -0,0 +1,3 @@ +{ + "serialNumber": "W874939YX92" +} -- cgit v1.2.3